mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 14:23:32 +00:00
[SG-58] Avatar color selector (#3691)
* changes * merge * undo * work * stuffs * chore: added custom color picker * oops * chore: everything but the broken sink * picker v2 * fix: cleanup * fix: linty * fix: use tailwind * fix: use tailwind * undo: merge error * remove: old color picker * fix: merge issue * chore: use input vs component * fix: move logic out! * fix: revert changes to bit-avatar * fix: cleanup undos * feat: color lookup for "me" badge in vault * fix: naming stuff * fix: event emitter * fix: linty * fix: protect * fix: remove v1 states work: navatar * fix: big * fix: messages merge issue * bug: differing bg colors for generated components * feat: added sync stuff * fix: cli * fix: remove service refs, use state * fix: moved from EventEmitter to Subjects * fix: srs * fix: strict stuff is nice tbh * SG-920 + SG-921 (#4342) * SG-920 + SG-921 * Update change-avatar.component.html * Update selectable-avatar.component.ts * [SG-926] [SG-58] [Defect] - Selected Avatar color does not persist in the Account Settings menu (#4359) * SG-926 * fix: comment * fix: undo * fix: imp * work: done with static values (#4272) * [SG-35] (#4361) Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/abstractions/appId.service";
|
||||
import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service";
|
||||
@@ -45,6 +46,7 @@ import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { StateFactory } from "@bitwarden/common/factories/stateFactory";
|
||||
import { GlobalState } from "@bitwarden/common/models/domain/global-state";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { AvatarUpdateService } from "@bitwarden/common/services/account/avatar-update.service";
|
||||
import { ApiService } from "@bitwarden/common/services/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/services/appId.service";
|
||||
import { AuditService } from "@bitwarden/common/services/audit.service";
|
||||
@@ -168,6 +170,7 @@ export default class MainBackground {
|
||||
policyApiService: PolicyApiServiceAbstraction;
|
||||
userVerificationApiService: UserVerificationApiServiceAbstraction;
|
||||
syncNotifierService: SyncNotifierServiceAbstraction;
|
||||
avatarUpdateService: AvatarUpdateServiceAbstraction;
|
||||
|
||||
// Passed to the popup for Safari to workaround issues with theming, downloading, etc.
|
||||
backgroundWindow = window;
|
||||
@@ -565,6 +568,8 @@ export default class MainBackground {
|
||||
this.stateService,
|
||||
this.apiService
|
||||
);
|
||||
|
||||
this.avatarUpdateService = new AvatarUpdateService(this.apiService, this.stateService);
|
||||
}
|
||||
|
||||
async bootstrap() {
|
||||
|
||||
@@ -97,6 +97,7 @@ export default class RuntimeBackground {
|
||||
await this.main.refreshBadge();
|
||||
await this.main.refreshMenu();
|
||||
}, 2000);
|
||||
this.main.avatarUpdateService.loadColorFromState();
|
||||
}
|
||||
break;
|
||||
case "openPopup":
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<app-avatar
|
||||
[text]="activeAccount.name"
|
||||
[id]="activeAccount.id"
|
||||
[color]="activeAccount.avatarColor"
|
||||
[size]="25"
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
@@ -65,6 +66,7 @@
|
||||
[circle]="true"
|
||||
[fontSize]="14"
|
||||
[dynamic]="true"
|
||||
[color]="a.value.avatarColor"
|
||||
*ngIf="a.value.profile.email != null"
|
||||
aria-hidden="true"
|
||||
></app-avatar>
|
||||
|
||||
@@ -16,6 +16,7 @@ type ActiveAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatarColor: string;
|
||||
};
|
||||
|
||||
export class SwitcherAccount extends Account {
|
||||
@@ -27,6 +28,8 @@ export class SwitcherAccount extends Account {
|
||||
);
|
||||
}
|
||||
|
||||
avatarColor: string;
|
||||
|
||||
private removeWebProtocolFromString(urlString: string) {
|
||||
const regex = /http(s)?(:)?(\/\/)?|(\/\/)?(www\.)?/g;
|
||||
return urlString.replace(regex, "");
|
||||
@@ -112,6 +115,7 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
id: await this.tokenService.getUserId(),
|
||||
name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()),
|
||||
email: await this.tokenService.getEmail(),
|
||||
avatarColor: await this.stateService.getAvatarColor(),
|
||||
};
|
||||
} catch {
|
||||
this.activeAccount = undefined;
|
||||
@@ -162,6 +166,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
|
||||
userId: userId,
|
||||
});
|
||||
switcherAccounts[userId] = new SwitcherAccount(baseAccounts[userId]);
|
||||
switcherAccounts[userId].avatarColor = await this.stateService.getAvatarColor({
|
||||
userId: userId,
|
||||
});
|
||||
}
|
||||
return switcherAccounts;
|
||||
}
|
||||
|
||||
41
apps/web/src/app/components/dynamic-avatar.component.ts
Normal file
41
apps/web/src/app/components/dynamic-avatar.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, Input, OnDestroy } from "@angular/core";
|
||||
import { Observable, Subject } from "rxjs";
|
||||
|
||||
import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
@Component({
|
||||
selector: "dynamic-avatar",
|
||||
template: `<span [title]="title">
|
||||
<bit-avatar
|
||||
appStopClick
|
||||
[text]="text"
|
||||
[size]="size"
|
||||
[color]="color$ | async"
|
||||
[border]="border"
|
||||
[id]="id"
|
||||
[title]="title"
|
||||
>
|
||||
</bit-avatar>
|
||||
</span>`,
|
||||
})
|
||||
export class DynamicAvatarComponent implements OnDestroy {
|
||||
@Input() border = false;
|
||||
@Input() id: string;
|
||||
@Input() text: string;
|
||||
@Input() title: string;
|
||||
@Input() size: SizeTypes = "default";
|
||||
color$: Observable<string | null>;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(private accountUpdateService: AvatarUpdateService) {
|
||||
if (this.text) {
|
||||
this.text = this.text.toUpperCase();
|
||||
}
|
||||
this.color$ = this.accountUpdateService.avatarUpdate$;
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
}
|
||||
54
apps/web/src/app/components/selectable-avatar.component.ts
Normal file
54
apps/web/src/app/components/selectable-avatar.component.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "selectable-avatar",
|
||||
template: `<span
|
||||
[title]="title"
|
||||
(click)="onFire()"
|
||||
(keyup.enter)="onFire()"
|
||||
tabindex="0"
|
||||
[ngClass]="classList"
|
||||
>
|
||||
<bit-avatar
|
||||
appStopClick
|
||||
[text]="text"
|
||||
size="xlarge"
|
||||
[text]="text"
|
||||
[color]="color"
|
||||
[border]="false"
|
||||
[id]="id"
|
||||
[border]="border"
|
||||
[title]="title"
|
||||
>
|
||||
</bit-avatar>
|
||||
</span>`,
|
||||
})
|
||||
export class SelectableAvatarComponent {
|
||||
@Input() id: string;
|
||||
@Input() text: string;
|
||||
@Input() title: string;
|
||||
@Input() color: string;
|
||||
@Input() border = false;
|
||||
@Input() selected = false;
|
||||
@Output() select = new EventEmitter<string>();
|
||||
|
||||
onFire() {
|
||||
this.select.emit(this.color);
|
||||
}
|
||||
|
||||
get classList() {
|
||||
return ["tw-rounded-full tw-inline-block"]
|
||||
.concat(["tw-cursor-pointer", "tw-outline", "tw-outline-solid", "tw-outline-offset-1"])
|
||||
.concat(
|
||||
this.selected
|
||||
? ["tw-outline-[3px]", "tw-outline-primary-500"]
|
||||
: [
|
||||
"tw-outline-0",
|
||||
"hover:tw-outline-1",
|
||||
"hover:tw-outline-primary-300",
|
||||
"focus:tw-outline-2",
|
||||
"focus:tw-outline-primary-500",
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@
|
||||
[bitMenuTriggerFor]="accountMenu"
|
||||
class="tw-border-0 tw-bg-transparent tw-text-alt2 tw-opacity-70 hover:tw-opacity-90"
|
||||
>
|
||||
<i class="bwi bwi-user-circle bwi-lg" aria-hidden="true"></i>
|
||||
<dynamic-avatar [text]="name" size="xsmall" aria-hidden="true"></dynamic-avatar>
|
||||
<i class="bwi bwi-caret-down bwi-sm" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu class="dropdown-menu" #accountMenu>
|
||||
@@ -55,7 +55,7 @@
|
||||
*ngIf="name"
|
||||
appStopProp
|
||||
>
|
||||
<bit-avatar [text]="name" [id]="userId" size="small"></bit-avatar>
|
||||
<dynamic-avatar [text]="name" size="small"></dynamic-avatar>
|
||||
<div class="tw-ml-2 tw-block tw-overflow-hidden tw-whitespace-nowrap">
|
||||
<span>{{ "loggedInAs" | i18n }}</span>
|
||||
<small class="tw-block tw-overflow-hidden tw-whitespace-nowrap tw-text-muted">{{
|
||||
|
||||
82
apps/web/src/app/settings/change-avatar.component.html
Normal file
82
apps/web/src/app/settings/change-avatar.component.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="customizeTitle">
|
||||
<div class="modal-dialog modal-dialog-scrollable tw-w-[600px] tw-max-w-none" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="customizeTitle">{{ "customizeAvatar" | i18n }}</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
data-dismiss="modal"
|
||||
appA11yTitle="{{ 'close' | i18n }}"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="card-body text-center" *ngIf="loading">
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
{{ "loading" | i18n }}
|
||||
</div>
|
||||
<app-callout type="error" *ngIf="error">
|
||||
{{ error }}
|
||||
</app-callout>
|
||||
<p class="tw-text-lg">{{ "pickAnAvatarColor" | i18n }}</p>
|
||||
<div class="tw-flex tw-flex-wrap tw-justify-center tw-gap-8 tw-gap-y-8">
|
||||
<ng-container *ngFor="let c of defaultColorPalette">
|
||||
<selectable-avatar
|
||||
appStopClick
|
||||
(select)="setSelection(c.color)"
|
||||
[selected]="c.selected"
|
||||
[title]="c.name"
|
||||
text="{{ profile | userName }}"
|
||||
[color]="c.color"
|
||||
[border]="true"
|
||||
>
|
||||
</selectable-avatar>
|
||||
</ng-container>
|
||||
<span>
|
||||
<span
|
||||
[tabIndex]="0"
|
||||
(keyup.enter)="showCustomPicker()"
|
||||
(click)="showCustomPicker()"
|
||||
title="{{ 'customColor' | i18n }}"
|
||||
[ngClass]="{
|
||||
'!tw-outline-[3px] tw-outline-primary-500 hover:tw-outline-[3px] hover:tw-outline-primary-500':
|
||||
customColorSelected
|
||||
}"
|
||||
class="tw-outline-solid tw-bg-white tw-relative tw-inline-block tw-flex tw-h-24 tw-w-24 tw-cursor-pointer tw-place-content-center tw-content-center tw-justify-center tw-rounded-full tw-border tw-border-solid tw-border-secondary-500 tw-outline tw-outline-0 tw-outline-offset-1 hover:tw-outline-1 hover:tw-outline-primary-300 focus:tw-outline-2 focus:tw-outline-primary-500"
|
||||
[style.background-color]="customColor$ | async"
|
||||
>
|
||||
<i
|
||||
[style.color]="customTextColor$ | async"
|
||||
class="bwi bwi-pencil tw-m-auto tw-text-3xl"
|
||||
></i>
|
||||
<input
|
||||
tabindex="-1"
|
||||
class="tw-absolute tw-right-0 tw-bottom-0 tw-h-px tw-w-px tw-border-none tw-bg-transparent tw-opacity-0"
|
||||
#colorPicker
|
||||
type="color"
|
||||
[ngModel]="customColor$ | async"
|
||||
(ngModelChange)="customColor$.next($event)"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary btn-submit"
|
||||
[disabled]="loading"
|
||||
(click)="submit()"
|
||||
>
|
||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
138
apps/web/src/app/settings/change-avatar.component.ts
Normal file
138
apps/web/src/app/settings/change-avatar.component.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
ViewEncapsulation,
|
||||
} from "@angular/core";
|
||||
import { BehaviorSubject, debounceTime, Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
|
||||
@Component({
|
||||
selector: "app-change-avatar",
|
||||
templateUrl: "change-avatar.component.html",
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
})
|
||||
export class ChangeAvatarComponent implements OnInit, OnDestroy {
|
||||
@Input() profile: ProfileResponse;
|
||||
|
||||
@Output() changeColor: EventEmitter<string | null> = new EventEmitter();
|
||||
@Output() onSaved = new EventEmitter();
|
||||
|
||||
@ViewChild("colorPicker") colorPickerElement: ElementRef<HTMLElement>;
|
||||
|
||||
loading = false;
|
||||
error: string;
|
||||
defaultColorPalette: NamedAvatarColor[] = [
|
||||
{ name: "brightBlue", color: "#16cbfc" },
|
||||
{ name: "green", color: "#94cc4b" },
|
||||
{ name: "orange", color: "#ffb520" },
|
||||
{ name: "lavender", color: "#e5beed" },
|
||||
{ name: "yellow", color: "#fcff41" },
|
||||
{ name: "indigo", color: "#acbdf7" },
|
||||
{ name: "teal", color: "#8ecdc5" },
|
||||
{ name: "salmon", color: "#ffa3a3" },
|
||||
{ name: "pink", color: "#ffa2d4" },
|
||||
];
|
||||
customColorSelected = false;
|
||||
currentSelection: string;
|
||||
|
||||
protected customColor$ = new BehaviorSubject<string | null>(null);
|
||||
protected customTextColor$ = new BehaviorSubject<string>("#000000");
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private accountUpdateService: AvatarUpdateService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
//localize the default colors
|
||||
this.defaultColorPalette.forEach((c) => (c.name = this.i18nService.t(c.name)));
|
||||
|
||||
this.customColor$
|
||||
.pipe(debounceTime(200), takeUntil(this.destroy$))
|
||||
.subscribe((color: string | null) => {
|
||||
if (color == null) {
|
||||
return;
|
||||
}
|
||||
this.customTextColor$.next(Utils.pickTextColorBasedOnBgColor(color));
|
||||
this.customColorSelected = true;
|
||||
this.currentSelection = color;
|
||||
});
|
||||
|
||||
this.setSelection(await this.accountUpdateService.loadColorFromState());
|
||||
}
|
||||
|
||||
async showCustomPicker() {
|
||||
this.customColorSelected = true;
|
||||
this.colorPickerElement.nativeElement.click();
|
||||
this.setSelection(this.customColor$.value);
|
||||
}
|
||||
|
||||
async generateAvatarColor() {
|
||||
Utils.stringToColor(this.profile.name.toString());
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
if (Utils.validateHexColor(this.currentSelection) || this.currentSelection == null) {
|
||||
await this.accountUpdateService.pushUpdate(this.currentSelection);
|
||||
this.changeColor.emit(this.currentSelection);
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("avatarUpdated"));
|
||||
} else {
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async setSelection(color: string | null) {
|
||||
this.defaultColorPalette.filter((x) => x.selected).forEach((c) => (c.selected = false));
|
||||
|
||||
if (color == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
color = color.toLowerCase();
|
||||
|
||||
this.customColorSelected = false;
|
||||
//Allow for toggle
|
||||
if (this.currentSelection === color) {
|
||||
this.currentSelection = null;
|
||||
} else {
|
||||
const selectedColorIndex = this.defaultColorPalette.findIndex((c) => c.color === color);
|
||||
if (selectedColorIndex !== -1) {
|
||||
this.defaultColorPalette[selectedColorIndex].selected = true;
|
||||
this.currentSelection = color;
|
||||
} else {
|
||||
this.customColor$.next(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NamedAvatarColor {
|
||||
name: string;
|
||||
color: string;
|
||||
selected? = false;
|
||||
}
|
||||
@@ -33,7 +33,17 @@
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="mb-3">
|
||||
<bit-avatar [text]="profile | userName" [id]="profile.id" size="large"></bit-avatar>
|
||||
<dynamic-avatar text="{{ profile | userName }}" [size]="'large'"> </dynamic-avatar>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary tw-ml-3.5"
|
||||
appStopClick
|
||||
appStopProp
|
||||
(click)="openChangeAvatar()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-pencil-square" aria-hidden="true"></i>
|
||||
Customize
|
||||
</button>
|
||||
</div>
|
||||
<hr />
|
||||
<p *ngIf="fingerprint">
|
||||
@@ -55,3 +65,4 @@
|
||||
<span>{{ "save" | i18n }}</span>
|
||||
</button>
|
||||
</form>
|
||||
<ng-template #avatarModalTemplate></ng-template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ViewChild, ViewContainerRef, Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
@@ -10,16 +12,21 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { UpdateProfileRequest } from "@bitwarden/common/models/request/update-profile.request";
|
||||
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
|
||||
|
||||
import { ChangeAvatarComponent } from "./change-avatar.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-profile",
|
||||
templateUrl: "profile.component.html",
|
||||
})
|
||||
export class ProfileComponent implements OnInit {
|
||||
export class ProfileComponent implements OnInit, OnDestroy {
|
||||
loading = true;
|
||||
profile: ProfileResponse;
|
||||
fingerprint: string;
|
||||
|
||||
formPromise: Promise<any>;
|
||||
@ViewChild("avatarModalTemplate", { read: ViewContainerRef, static: true })
|
||||
avatarModalRef: ViewContainerRef;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -28,7 +35,8 @@ export class ProfileComponent implements OnInit {
|
||||
private cryptoService: CryptoService,
|
||||
private logService: LogService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
private stateService: StateService
|
||||
private stateService: StateService,
|
||||
private modalService: ModalService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -42,6 +50,24 @@ export class ProfileComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async openChangeAvatar() {
|
||||
const modalOpened = await this.modalService.openViewRef(
|
||||
ChangeAvatarComponent,
|
||||
this.avatarModalRef,
|
||||
(modal) => {
|
||||
modal.profile = this.profile;
|
||||
modal.changeColor.pipe(takeUntil(this.destroy$)).subscribe(() => {
|
||||
modalOpened[0].close();
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async submit() {
|
||||
try {
|
||||
const request = new UpdateProfileRequest(this.profile.name, this.profile.masterPasswordHint);
|
||||
|
||||
@@ -16,10 +16,12 @@ import { UpdatePasswordComponent } from "../accounts/update-password.component";
|
||||
import { UpdateTempPasswordComponent } from "../accounts/update-temp-password.component";
|
||||
import { VerifyEmailTokenComponent } from "../accounts/verify-email-token.component";
|
||||
import { VerifyRecoverDeleteComponent } from "../accounts/verify-recover-delete.component";
|
||||
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
|
||||
import { NestedCheckboxComponent } from "../components/nested-checkbox.component";
|
||||
import { OrganizationSwitcherComponent } from "../components/organization-switcher.component";
|
||||
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
|
||||
import { PremiumBadgeComponent } from "../components/premium-badge.component";
|
||||
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
|
||||
import { UserVerificationPromptComponent } from "../components/user-verification-prompt.component";
|
||||
import { UserVerificationComponent } from "../components/user-verification.component";
|
||||
import { FooterComponent } from "../layouts/footer.component";
|
||||
@@ -69,6 +71,7 @@ import { ApiKeyComponent } from "../settings/api-key.component";
|
||||
import { BillingHistoryViewComponent } from "../settings/billing-history-view.component";
|
||||
import { BillingHistoryComponent } from "../settings/billing-history.component";
|
||||
import { BillingSyncKeyComponent } from "../settings/billing-sync-key.component";
|
||||
import { ChangeAvatarComponent } from "../settings/change-avatar.component";
|
||||
import { ChangeEmailComponent } from "../settings/change-email.component";
|
||||
import { ChangeKdfComponent } from "../settings/change-kdf.component";
|
||||
import { ChangePasswordComponent } from "../settings/change-password.component";
|
||||
@@ -167,6 +170,7 @@ import { SharedModule } from ".";
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DomainRulesComponent,
|
||||
DynamicAvatarComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
@@ -220,6 +224,7 @@ import { SharedModule } from ".";
|
||||
PremiumBadgeComponent,
|
||||
PremiumComponent,
|
||||
ProfileComponent,
|
||||
ChangeAvatarComponent,
|
||||
ProvidersComponent,
|
||||
PurgeVaultComponent,
|
||||
RecoverDeleteComponent,
|
||||
@@ -227,6 +232,7 @@ import { SharedModule } from ".";
|
||||
RemovePasswordComponent,
|
||||
SecurityComponent,
|
||||
SecurityKeysComponent,
|
||||
SelectableAvatarComponent,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SendEffluxDatesComponent,
|
||||
@@ -290,6 +296,7 @@ import { SharedModule } from ".";
|
||||
DeauthorizeSessionsComponent,
|
||||
DeleteAccountComponent,
|
||||
DomainRulesComponent,
|
||||
DynamicAvatarComponent,
|
||||
EmergencyAccessAddEditComponent,
|
||||
EmergencyAccessAttachmentsComponent,
|
||||
EmergencyAccessComponent,
|
||||
@@ -342,6 +349,7 @@ import { SharedModule } from ".";
|
||||
PremiumBadgeComponent,
|
||||
PremiumComponent,
|
||||
ProfileComponent,
|
||||
ChangeAvatarComponent,
|
||||
ProvidersComponent,
|
||||
PurgeVaultComponent,
|
||||
RecoverDeleteComponent,
|
||||
@@ -349,6 +357,7 @@ import { SharedModule } from ".";
|
||||
RemovePasswordComponent,
|
||||
SecurityComponent,
|
||||
SecurityKeysComponent,
|
||||
SelectableAvatarComponent,
|
||||
SendAddEditComponent,
|
||||
SendComponent,
|
||||
SendEffluxDatesComponent,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
||||
|
||||
import { AvatarUpdateService } from "@bitwarden/common/abstractions/account/avatar-update.service";
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { TokenService } from "@bitwarden/common/abstractions/token.service";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
|
||||
@Component({
|
||||
@@ -15,18 +17,29 @@ export class OrganizationNameBadgeComponent implements OnInit {
|
||||
|
||||
color: string;
|
||||
textColor: string;
|
||||
isMe: boolean;
|
||||
|
||||
constructor(private i18nService: I18nService) {}
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private avatarService: AvatarUpdateService,
|
||||
private tokenService: TokenService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (this.organizationName == null || this.organizationName === "") {
|
||||
this.organizationName = this.i18nService.t("me");
|
||||
this.color = Utils.stringToColor(this.profileName.toUpperCase());
|
||||
this.isMe = true;
|
||||
}
|
||||
if (this.color == null) {
|
||||
this.color = Utils.stringToColor(this.organizationName.toUpperCase());
|
||||
if (this.isMe) {
|
||||
this.color = await this.avatarService.loadColorFromState();
|
||||
if (this.color == null) {
|
||||
const userName = await this.tokenService.getName();
|
||||
this.color = Utils.stringToColor(userName.toUpperCase());
|
||||
}
|
||||
} else {
|
||||
this.color = Utils.stringToColor(this.organizationName);
|
||||
}
|
||||
this.textColor = Utils.pickTextColorBasedOnBgColor(this.color);
|
||||
this.textColor = Utils.pickTextColorBasedOnBgColor(this.color, 135, true) + "!important";
|
||||
}
|
||||
|
||||
emitOnOrganizationClicked() {
|
||||
|
||||
@@ -5475,6 +5475,45 @@
|
||||
"notYou": {
|
||||
"message": "Not you?"
|
||||
},
|
||||
"pickAnAvatarColor": {
|
||||
"message": "Pick an avatar color"
|
||||
},
|
||||
"customizeAvatar": {
|
||||
"message": "Customize avatar"
|
||||
},
|
||||
"avatarUpdated": {
|
||||
"message": "Avatar updated"
|
||||
},
|
||||
"brightBlue": {
|
||||
"message": "Bright Blue"
|
||||
},
|
||||
"green": {
|
||||
"message": "Green"
|
||||
},
|
||||
"orange": {
|
||||
"message": "Orange"
|
||||
},
|
||||
"lavender": {
|
||||
"message": "Lavender"
|
||||
},
|
||||
"yellow": {
|
||||
"message": "Yellow"
|
||||
},
|
||||
"indigo": {
|
||||
"message": "Indigo"
|
||||
},
|
||||
"teal": {
|
||||
"message": "Teal"
|
||||
},
|
||||
"salmon": {
|
||||
"message": "Salmon"
|
||||
},
|
||||
"pink": {
|
||||
"message": "Pink"
|
||||
},
|
||||
"customColor": {
|
||||
"message": "Custom Color"
|
||||
},
|
||||
"multiSelectPlaceholder": {
|
||||
"message": "-- Type to Filter --"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user