From d212bb1fd08b24f345bbb45423260f902b8ad750 Mon Sep 17 00:00:00 2001
From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com>
Date: Thu, 8 Aug 2024 17:01:47 -0500
Subject: [PATCH] [PM-7713] Refresh Appearance Settings (#10458)
* add v2 of appearance component
* swap in new appearance component based on refresh flag
* update default theme verbiage
---
apps/browser/src/_locales/en/messages.json | 6 +
apps/browser/src/popup/app-routing.module.ts | 6 +-
.../settings/appearance-v2.component.html | 32 +++++
.../settings/appearance-v2.component.spec.ts | 110 ++++++++++++++++++
.../popup/settings/appearance-v2.component.ts | 110 ++++++++++++++++++
5 files changed, 261 insertions(+), 3 deletions(-)
create mode 100644 apps/browser/src/vault/popup/settings/appearance-v2.component.html
create mode 100644 apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts
create mode 100644 apps/browser/src/vault/popup/settings/appearance-v2.component.ts
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 6c41706ddd7..1af1cb66387 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -4164,5 +4164,11 @@
},
"accountActions": {
"message": "Account actions"
+ },
+ "showNumberOfAutofillSuggestions": {
+ "message": "Show number of login autofill suggestions on extension icon"
+ },
+ "systemDefault": {
+ "message": "System default"
}
}
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index 16e12c3d75c..b7b4e41778b 100644
--- a/apps/browser/src/popup/app-routing.module.ts
+++ b/apps/browser/src/popup/app-routing.module.ts
@@ -79,6 +79,7 @@ import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
+import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
@@ -339,12 +340,11 @@ const routes: Routes = [
canActivate: [authGuard],
data: { state: "premium" },
},
- {
+ ...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, {
path: "appearance",
- component: AppearanceComponent,
canActivate: [authGuard],
data: { state: "appearance" },
- },
+ }),
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
path: "clone-cipher",
canActivate: [authGuard],
diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.html b/apps/browser/src/vault/popup/settings/appearance-v2.component.html
new file mode 100644
index 00000000000..565699a6f5b
--- /dev/null
+++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts
new file mode 100644
index 00000000000..69186359e2b
--- /dev/null
+++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.spec.ts
@@ -0,0 +1,110 @@
+import { Component, Input } from "@angular/core";
+import { ComponentFixture, TestBed } from "@angular/core/testing";
+import { mock } from "jest-mock-extended";
+import { BehaviorSubject } from "rxjs";
+
+import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
+import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
+import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { ThemeType } from "@bitwarden/common/platform/enums";
+import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
+
+import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
+
+import { AppearanceV2Component } from "./appearance-v2.component";
+
+@Component({
+ standalone: true,
+ selector: "popup-header",
+ template: ``,
+})
+class MockPopupHeaderComponent {
+ @Input() pageTitle: string;
+ @Input() backAction: () => void;
+}
+
+@Component({
+ standalone: true,
+ selector: "popup-page",
+ template: ``,
+})
+class MockPopupPageComponent {}
+
+describe("AppearanceV2Component", () => {
+ let component: AppearanceV2Component;
+ let fixture: ComponentFixture;
+
+ const showFavicons$ = new BehaviorSubject(true);
+ const enableBadgeCounter$ = new BehaviorSubject(true);
+ const selectedTheme$ = new BehaviorSubject(ThemeType.Nord);
+ const setSelectedTheme = jest.fn().mockResolvedValue(undefined);
+ const setShowFavicons = jest.fn().mockResolvedValue(undefined);
+ const setEnableBadgeCounter = jest.fn().mockResolvedValue(undefined);
+
+ beforeEach(async () => {
+ setSelectedTheme.mockClear();
+ setShowFavicons.mockClear();
+ setEnableBadgeCounter.mockClear();
+
+ await TestBed.configureTestingModule({
+ imports: [AppearanceV2Component],
+ providers: [
+ { provide: ConfigService, useValue: mock() },
+ { provide: PlatformUtilsService, useValue: mock() },
+ { provide: MessagingService, useValue: mock() },
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ { provide: DomainSettingsService, useValue: { showFavicons$, setShowFavicons } },
+ {
+ provide: BadgeSettingsServiceAbstraction,
+ useValue: { enableBadgeCounter$, setEnableBadgeCounter },
+ },
+ { provide: ThemeStateService, useValue: { selectedTheme$, setSelectedTheme } },
+ ],
+ })
+ .overrideComponent(AppearanceV2Component, {
+ remove: {
+ imports: [PopupHeaderComponent, PopupPageComponent],
+ },
+ add: {
+ imports: [MockPopupHeaderComponent, MockPopupPageComponent],
+ },
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AppearanceV2Component);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("populates the form with the user's current settings", () => {
+ expect(component.appearanceForm.value).toEqual({
+ enableFavicon: true,
+ enableBadgeCounter: true,
+ theme: ThemeType.Nord,
+ });
+ });
+
+ describe("form changes", () => {
+ it("updates the users theme", () => {
+ component.appearanceForm.controls.theme.setValue(ThemeType.Light);
+
+ expect(setSelectedTheme).toHaveBeenCalledWith(ThemeType.Light);
+ });
+
+ it("updates the users favicon setting", () => {
+ component.appearanceForm.controls.enableFavicon.setValue(false);
+
+ expect(setShowFavicons).toHaveBeenCalledWith(false);
+ });
+
+ it("updates the users badge counter setting", () => {
+ component.appearanceForm.controls.enableBadgeCounter.setValue(false);
+
+ expect(setEnableBadgeCounter).toHaveBeenCalledWith(false);
+ });
+ });
+});
diff --git a/apps/browser/src/vault/popup/settings/appearance-v2.component.ts b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts
new file mode 100644
index 00000000000..7ca073d51b0
--- /dev/null
+++ b/apps/browser/src/vault/popup/settings/appearance-v2.component.ts
@@ -0,0 +1,110 @@
+import { CommonModule } from "@angular/common";
+import { Component, DestroyRef, OnInit } from "@angular/core";
+import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
+import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
+import { firstValueFrom } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
+import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
+import { ThemeType } from "@bitwarden/common/platform/enums";
+import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
+import { CheckboxModule } from "@bitwarden/components";
+
+import { CardComponent } from "../../../../../../libs/components/src/card/card.component";
+import { FormFieldModule } from "../../../../../../libs/components/src/form-field/form-field.module";
+import { SelectModule } from "../../../../../../libs/components/src/select/select.module";
+import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
+import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
+
+@Component({
+ standalone: true,
+ templateUrl: "./appearance-v2.component.html",
+ imports: [
+ CommonModule,
+ JslibModule,
+ PopupPageComponent,
+ PopupHeaderComponent,
+ PopOutComponent,
+ CardComponent,
+ FormFieldModule,
+ SelectModule,
+ ReactiveFormsModule,
+ CheckboxModule,
+ ],
+})
+export class AppearanceV2Component implements OnInit {
+ appearanceForm = this.formBuilder.group({
+ enableFavicon: false,
+ enableBadgeCounter: true,
+ theme: ThemeType.System,
+ });
+
+ /** Available theme options */
+ themeOptions: { name: string; value: ThemeType }[];
+
+ constructor(
+ private messagingService: MessagingService,
+ private domainSettingsService: DomainSettingsService,
+ private badgeSettingsService: BadgeSettingsServiceAbstraction,
+ private themeStateService: ThemeStateService,
+ private formBuilder: FormBuilder,
+ private destroyRef: DestroyRef,
+ i18nService: I18nService,
+ ) {
+ this.themeOptions = [
+ { name: i18nService.t("systemDefault"), value: ThemeType.System },
+ { name: i18nService.t("light"), value: ThemeType.Light },
+ { name: i18nService.t("dark"), value: ThemeType.Dark },
+ { name: "Nord", value: ThemeType.Nord },
+ { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
+ ];
+ }
+
+ async ngOnInit() {
+ const enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
+ const enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
+ const theme = await firstValueFrom(this.themeStateService.selectedTheme$);
+
+ // Set initial values for the form
+ this.appearanceForm.setValue({
+ enableFavicon,
+ enableBadgeCounter,
+ theme,
+ });
+
+ this.appearanceForm.controls.theme.valueChanges
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((newTheme) => {
+ void this.saveTheme(newTheme);
+ });
+
+ this.appearanceForm.controls.enableFavicon.valueChanges
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((enableFavicon) => {
+ void this.updateFavicon(enableFavicon);
+ });
+
+ this.appearanceForm.controls.enableBadgeCounter.valueChanges
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((enableBadgeCounter) => {
+ void this.updateBadgeCounter(enableBadgeCounter);
+ });
+ }
+
+ async updateFavicon(enableFavicon: boolean) {
+ await this.domainSettingsService.setShowFavicons(enableFavicon);
+ }
+
+ async updateBadgeCounter(enableBadgeCounter: boolean) {
+ await this.badgeSettingsService.setEnableBadgeCounter(enableBadgeCounter);
+ this.messagingService.send("bgUpdateContextMenu");
+ }
+
+ async saveTheme(newTheme: ThemeType) {
+ await this.themeStateService.setSelectedTheme(newTheme);
+ }
+}