mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +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:
@@ -0,0 +1,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
export abstract class AvatarUpdateService {
|
||||
avatarUpdate$ = new Observable<string | null>();
|
||||
abstract pushUpdate(color: string): Promise<ProfileResponse | void>;
|
||||
abstract loadColorFromState(): Promise<string | null>;
|
||||
}
|
||||
@@ -62,6 +62,7 @@ import { TaxInfoUpdateRequest } from "../models/request/tax-info-update.request"
|
||||
import { TwoFactorEmailRequest } from "../models/request/two-factor-email.request";
|
||||
import { TwoFactorProviderRequest } from "../models/request/two-factor-provider.request";
|
||||
import { TwoFactorRecoveryRequest } from "../models/request/two-factor-recovery.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
|
||||
import { UpdateKeyRequest } from "../models/request/update-key.request";
|
||||
import { UpdateProfileRequest } from "../models/request/update-profile.request";
|
||||
@@ -172,6 +173,7 @@ export abstract class ApiService {
|
||||
getUserSubscription: () => Promise<SubscriptionResponse>;
|
||||
getTaxInfo: () => Promise<TaxInfoResponse>;
|
||||
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
|
||||
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
|
||||
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
|
||||
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
|
||||
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
|
||||
|
||||
@@ -349,4 +349,7 @@ export abstract class StateService<T extends Account = Account> {
|
||||
* @deprecated Do not call this directly, use ConfigService
|
||||
*/
|
||||
setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise<void>;
|
||||
|
||||
getAvatarColor: (options?: StorageOptions) => Promise<string | null | undefined>;
|
||||
setAvatarColor: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -431,6 +431,10 @@ export class Utils {
|
||||
return this.global.bitwardenContainerService;
|
||||
}
|
||||
|
||||
static validateHexColor(color: string) {
|
||||
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts map to a Record<string, V> with the same data. Inverse of recordToMap
|
||||
* Useful in toJSON methods, since Maps are not serializable
|
||||
|
||||
@@ -233,6 +233,7 @@ export class AccountSettings {
|
||||
vaultTimeout?: number;
|
||||
vaultTimeoutAction?: string = "lock";
|
||||
serverConfig?: ServerConfigData;
|
||||
avatarColor?: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||
if (obj == null) {
|
||||
|
||||
7
libs/common/src/models/request/update-avatar.request.ts
Normal file
7
libs/common/src/models/request/update-avatar.request.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class UpdateAvatarRequest {
|
||||
avatarColor: string;
|
||||
|
||||
constructor(avatarColor: string) {
|
||||
this.avatarColor = avatarColor;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
culture: string;
|
||||
twoFactorEnabled: boolean;
|
||||
key: string;
|
||||
avatarColor: string;
|
||||
privateKey: string;
|
||||
securityStamp: string;
|
||||
forcePasswordReset: boolean;
|
||||
@@ -34,6 +35,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
this.culture = this.getResponseProperty("Culture");
|
||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||
this.key = this.getResponseProperty("Key");
|
||||
this.avatarColor = this.getResponseProperty("AvatarColor");
|
||||
this.privateKey = this.getResponseProperty("PrivateKey");
|
||||
this.securityStamp = this.getResponseProperty("SecurityStamp");
|
||||
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
|
||||
|
||||
30
libs/common/src/services/account/avatar-update.service.ts
Normal file
30
libs/common/src/services/account/avatar-update.service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
|
||||
import { AvatarUpdateService as AvatarUpdateServiceAbstraction } from "../../abstractions/account/avatar-update.service";
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { StateService } from "../../abstractions/state.service";
|
||||
import { UpdateAvatarRequest } from "../../models/request/update-avatar.request";
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
|
||||
export class AvatarUpdateService implements AvatarUpdateServiceAbstraction {
|
||||
private _avatarUpdate$ = new BehaviorSubject<string | null>(null);
|
||||
avatarUpdate$: Observable<string | null> = this._avatarUpdate$.asObservable();
|
||||
|
||||
constructor(private apiService: ApiService, private stateService: StateService) {
|
||||
this.loadColorFromState();
|
||||
}
|
||||
|
||||
loadColorFromState(): Promise<string | null> {
|
||||
return this.stateService.getAvatarColor().then((color) => {
|
||||
this._avatarUpdate$.next(color);
|
||||
return color;
|
||||
});
|
||||
}
|
||||
|
||||
pushUpdate(color: string | null): Promise<ProfileResponse | void> {
|
||||
return this.apiService.putAvatar(new UpdateAvatarRequest(color)).then((response) => {
|
||||
this.stateService.setAvatarColor(response.avatarColor);
|
||||
this._avatarUpdate$.next(response.avatarColor);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,7 @@ import { TaxInfoUpdateRequest } from "../models/request/tax-info-update.request"
|
||||
import { TwoFactorEmailRequest } from "../models/request/two-factor-email.request";
|
||||
import { TwoFactorProviderRequest } from "../models/request/two-factor-provider.request";
|
||||
import { TwoFactorRecoveryRequest } from "../models/request/two-factor-recovery.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
|
||||
import { UpdateKeyRequest } from "../models/request/update-key.request";
|
||||
import { UpdateProfileRequest } from "../models/request/update-profile.request";
|
||||
@@ -290,6 +291,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
return new ProfileResponse(r);
|
||||
}
|
||||
|
||||
async putAvatar(request: UpdateAvatarRequest): Promise<ProfileResponse> {
|
||||
const r = await this.send("PUT", "/accounts/avatar", request, true, true);
|
||||
return new ProfileResponse(r);
|
||||
}
|
||||
|
||||
putTaxInfo(request: TaxInfoUpdateRequest): Promise<any> {
|
||||
return this.send("PUT", "/accounts/tax", request, true, false);
|
||||
}
|
||||
|
||||
@@ -2301,6 +2301,23 @@ export class StateService<
|
||||
)?.settings?.serverConfig;
|
||||
}
|
||||
|
||||
async getAvatarColor(options?: StorageOptions): Promise<string | null | undefined> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
)?.settings?.avatarColor;
|
||||
}
|
||||
|
||||
async setAvatarColor(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
account.settings.avatarColor = value;
|
||||
return await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||
);
|
||||
}
|
||||
|
||||
protected async getGlobals(options: StorageOptions): Promise<TGlobalState> {
|
||||
let globals: TGlobalState;
|
||||
if (this.useMemory(options.storageLocation)) {
|
||||
|
||||
@@ -304,6 +304,7 @@ export class SyncService implements SyncServiceAbstraction {
|
||||
await this.cryptoService.setEncPrivateKey(response.privateKey);
|
||||
await this.cryptoService.setProviderKeys(response.providers);
|
||||
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
|
||||
await this.stateService.setAvatarColor(response.avatarColor);
|
||||
await this.stateService.setSecurityStamp(response.securityStamp);
|
||||
await this.stateService.setEmailVerified(response.emailVerified);
|
||||
await this.stateService.setHasPremiumPersonally(response.premiumPersonally);
|
||||
|
||||
Reference in New Issue
Block a user