From d84c011a8c0ab47c7486a2ea913980a55fd374d2 Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 12 Aug 2025 11:02:28 +0200 Subject: [PATCH] feat: add experimental version of a callback solution --- .../autofill-badge-updater.service.ts | 192 +++++++++++------- .../src/platform/badge/badge.service.ts | 61 +++++- 2 files changed, 170 insertions(+), 83 deletions(-) diff --git a/apps/browser/src/autofill/services/autofill-badge-updater.service.ts b/apps/browser/src/autofill/services/autofill-badge-updater.service.ts index 42cb8886216..ff5ffe7883d 100644 --- a/apps/browser/src/autofill/services/autofill-badge-updater.service.ts +++ b/apps/browser/src/autofill/services/autofill-badge-updater.service.ts @@ -1,4 +1,4 @@ -import { combineLatest, distinctUntilChanged, mergeMap, of, Subject, switchMap } from "rxjs"; +import { combineLatest, distinctUntilChanged, mergeMap, Subject } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; @@ -6,7 +6,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { BadgeService } from "../../platform/badge/badge.service"; +import { BadgeService, StateSetting } from "../../platform/badge/badge.service"; import { BadgeStatePriority } from "../../platform/badge/priority"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -24,89 +24,129 @@ export class AutofillBadgeUpdaterService { private badgeSettingsService: BadgeSettingsServiceAbstraction, private logService: LogService, ) { - const cipherViews$ = this.accountService.activeAccount$.pipe( - switchMap((account) => (account?.id ? this.cipherService.cipherViews$(account?.id) : of([]))), - ); - - combineLatest({ - account: this.accountService.activeAccount$, - enableBadgeCounter: - this.badgeSettingsService.enableBadgeCounter$.pipe(distinctUntilChanged()), - ciphers: cipherViews$, - }) - .pipe( - mergeMap(async ({ account, enableBadgeCounter, ciphers }) => { - if (!account) { - return; - } - - const tabs = await BrowserApi.tabsQuery({}); - for (const tab of tabs) { - if (!tab.id) { - continue; - } - - if (enableBadgeCounter) { - await this.setTabState(tab, account.id); - } else { - await this.clearTabState(tab.id); - } - } - }), - ) - .subscribe(); - - combineLatest({ - account: this.accountService.activeAccount$, - enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$, - replaced: this.tabReplaced$, - ciphers: cipherViews$, - }) - .pipe( - mergeMap(async ({ account, enableBadgeCounter, replaced }) => { + this.badgeService.setDynamicState("autofill-state", (tab) => { + return combineLatest({ + account: this.accountService.activeAccount$, + enableBadgeCounter: + this.badgeSettingsService.enableBadgeCounter$.pipe(distinctUntilChanged()), + }).pipe( + mergeMap(async ({ account, enableBadgeCounter }) => { if (!account || !enableBadgeCounter) { - return; + return { + priority: BadgeStatePriority.Default, + state: {}, + tabId: tab.id, + } satisfies StateSetting; } - await this.clearTabState(replaced.removedTabId); - await this.setTabState(replaced.addedTab, account.id); - }), - ) - .subscribe(); + const ciphers = tab.url + ? await this.cipherService.getAllDecryptedForUrl(tab.url, account.id) + : []; + const cipherCount = ciphers.length; - combineLatest({ - account: this.accountService.activeAccount$, - enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$, - tab: this.tabUpdated$, - ciphers: cipherViews$, - }) - .pipe( - mergeMap(async ({ account, enableBadgeCounter, tab }) => { - if (!account || !enableBadgeCounter) { - return; + if (cipherCount === 0) { + return { + priority: BadgeStatePriority.Default, + state: { text: "0" }, + tabId: tab.id, + }; } - await this.setTabState(tab, account.id); + const countText = cipherCount > 9 ? "9+" : cipherCount.toString(); + return { + priority: BadgeStatePriority.Default, + state: { + text: countText, + }, + tabId: tab.id, + }; }), - ) - .subscribe(); + ); + }); - combineLatest({ - account: this.accountService.activeAccount$, - enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$, - tabId: this.tabRemoved$, - ciphers: cipherViews$, - }) - .pipe( - mergeMap(async ({ account, enableBadgeCounter, tabId }) => { - if (!account || !enableBadgeCounter) { - return; - } + // const cipherViews$ = this.accountService.activeAccount$.pipe( + // switchMap((account) => (account?.id ? this.cipherService.cipherViews$(account?.id) : of([]))), + // ); - await this.clearTabState(tabId); - }), - ) - .subscribe(); + // combineLatest({ + // account: this.accountService.activeAccount$, + // enableBadgeCounter: + // this.badgeSettingsService.enableBadgeCounter$.pipe(distinctUntilChanged()), + // ciphers: cipherViews$, + // }) + // .pipe( + // mergeMap(async ({ account, enableBadgeCounter, ciphers }) => { + // if (!account) { + // return; + // } + + // const tabs = await BrowserApi.tabsQuery({}); + // for (const tab of tabs) { + // if (!tab.id) { + // continue; + // } + + // if (enableBadgeCounter) { + // await this.setTabState(tab, account.id); + // } else { + // await this.clearTabState(tab.id); + // } + // } + // }), + // ) + // .subscribe(); + + // combineLatest({ + // account: this.accountService.activeAccount$, + // enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$, + // replaced: this.tabReplaced$, + // ciphers: cipherViews$, + // }) + // .pipe( + // mergeMap(async ({ account, enableBadgeCounter, replaced }) => { + // if (!account || !enableBadgeCounter) { + // return; + // } + + // await this.clearTabState(replaced.removedTabId); + // await this.setTabState(replaced.addedTab, account.id); + // }), + // ) + // .subscribe(); + + // combineLatest({ + // account: this.accountService.activeAccount$, + // enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$, + // tab: this.tabUpdated$, + // ciphers: cipherViews$, + // }) + // .pipe( + // mergeMap(async ({ account, enableBadgeCounter, tab }) => { + // if (!account || !enableBadgeCounter) { + // return; + // } + + // await this.setTabState(tab, account.id); + // }), + // ) + // .subscribe(); + + // combineLatest({ + // account: this.accountService.activeAccount$, + // enableBadgeCounter: this.badgeSettingsService.enableBadgeCounter$, + // tabId: this.tabRemoved$, + // ciphers: cipherViews$, + // }) + // .pipe( + // mergeMap(async ({ account, enableBadgeCounter, tabId }) => { + // if (!account || !enableBadgeCounter) { + // return; + // } + + // await this.clearTabState(tabId); + // }), + // ) + // .subscribe(); } init() { diff --git a/apps/browser/src/platform/badge/badge.service.ts b/apps/browser/src/platform/badge/badge.service.ts index c6836a3e0b9..349ea7f55a9 100644 --- a/apps/browser/src/platform/badge/badge.service.ts +++ b/apps/browser/src/platform/badge/badge.service.ts @@ -1,4 +1,15 @@ -import { concatMap, firstValueFrom, Subscription, withLatestFrom } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + concatMap, + EMPTY, + firstValueFrom, + map, + Observable, + Subscription, + switchMap, + withLatestFrom, +} from "rxjs"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { @@ -8,23 +19,28 @@ import { StateProvider, } from "@bitwarden/common/platform/state"; +import { BrowserApi } from "../browser/browser-api"; + import { BadgeBrowserApi, RawBadgeState } from "./badge-browser-api"; import { DefaultBadgeState } from "./consts"; import { BadgeStatePriority } from "./priority"; import { BadgeState, Unset } from "./state"; -interface StateSetting { +export interface StateSetting { priority: BadgeStatePriority; state: BadgeState; tabId?: number; } const BADGE_STATES = new KeyDefinition(BADGE_MEMORY, "badgeStates", { - deserializer: (value: Record) => value ?? { states: {} }, + deserializer: (value: Record) => value ?? {}, }); export class BadgeService { private serviceState: GlobalState>; + private stateCalculators = new BehaviorSubject< + Record Observable> + >({}); constructor( private stateProvider: StateProvider, @@ -40,11 +56,32 @@ export class BadgeService { */ startListening(): Subscription { // React to tab changes - return this.badgeApi.activeTab$ + return combineLatest([this.badgeApi.activeTab$, this.stateCalculators]) .pipe( + concatMap(async ([activeTab, stateCalculators]) => { + if (activeTab == undefined) { + return { tab: undefined, stateCalculators }; + } + + return { activeTab: await BrowserApi.getTab(activeTab.tabId), stateCalculators }; + }), + switchMap(({ activeTab, stateCalculators }) => { + if (activeTab == undefined) { + return EMPTY; + } + + return combineLatest( + Object.values(stateCalculators).map((calculator) => calculator(activeTab)), + ).pipe(map((states) => ({ activeTab, states }))); + }), withLatestFrom(this.serviceState.state$), - concatMap(async ([activeTab, serviceState]) => { - await this.updateBadge(activeTab, serviceState, activeTab?.tabId); + concatMap(async ([{ activeTab, states }, serviceState]) => { + const allStates = [...Object.values(serviceState ?? {}), ...states]; + await this.updateBadge( + { tabId: activeTab.id ?? 0, windowId: activeTab.windowId }, + allStates, + activeTab?.id, + ); }), ) .subscribe(); @@ -75,6 +112,16 @@ export class BadgeService { await this.updateBadge(activeTab, newServiceState, tabId); } + setDynamicState( + name: string, + calculatorFactory: (tab: chrome.tabs.Tab) => Observable, + ) { + this.stateCalculators.next({ + ...this.stateCalculators.value, + [name]: calculatorFactory, + }); + } + /** * Clear the state with the given name. * @@ -145,7 +192,7 @@ export class BadgeService { */ private async updateBadge( activeTab: chrome.tabs.TabActiveInfo | null | undefined, - serviceState: Record | null | undefined, + serviceState: Record | null | undefined | Array, tabId: number | undefined, ) { if (activeTab === undefined) {