1
0
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:
Brandon Maharaj
2023-01-01 10:30:09 -05:00
committed by GitHub
parent 0a734ce338
commit d41b3b13ea
27 changed files with 533 additions and 14 deletions

View File

@@ -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>;
}

View File

@@ -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>;

View File

@@ -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>;
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -0,0 +1,7 @@
export class UpdateAvatarRequest {
avatarColor: string;
constructor(avatarColor: string) {
this.avatarColor = avatarColor;
}
}

View File

@@ -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;

View 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);
});
}
}

View File

@@ -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);
}

View File

@@ -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)) {

View File

@@ -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);