mirror of
https://github.com/bitwarden/browser
synced 2026-02-20 03:13:55 +00:00
[PM-24615][PM-24999] Adjust autofill badge updater to only calculate the active tab (#16163)
* wip * feat: refactor how we react to tab changes * feat: always begin me emitting all active tabs * feat: only calculate autofill for active tabs * fix: bug not properly listening to reloads * wip * fix: clean up * fix: clean up
This commit is contained in:
@@ -15,13 +15,24 @@ export interface RawBadgeState {
|
||||
icon: BadgeIcon;
|
||||
}
|
||||
|
||||
export interface Tab {
|
||||
tabId: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
function tabFromChromeTab(tab: chrome.tabs.Tab): Tab {
|
||||
return {
|
||||
tabId: tab.id!,
|
||||
url: tab.url!,
|
||||
};
|
||||
}
|
||||
|
||||
export interface BadgeBrowserApi {
|
||||
activeTab$: Observable<chrome.tabs.TabActiveInfo | undefined>;
|
||||
// activeTabs$: Observable<chrome.tabs.Tab[]>;
|
||||
activeTabsUpdated$: Observable<Tab[]>;
|
||||
|
||||
setState(state: RawBadgeState, tabId?: number): Promise<void>;
|
||||
getTabs(): Promise<number[]>;
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]>;
|
||||
getActiveTabs(): Promise<Tab[]>;
|
||||
}
|
||||
|
||||
export class DefaultBadgeBrowserApi implements BadgeBrowserApi {
|
||||
@@ -29,34 +40,48 @@ export class DefaultBadgeBrowserApi implements BadgeBrowserApi {
|
||||
private sidebarAction = BrowserApi.getSidebarAction(self);
|
||||
|
||||
private onTabActivated$ = fromChromeEvent(chrome.tabs.onActivated).pipe(
|
||||
switchMap(async ([activeInfo]) => activeInfo),
|
||||
map(([activeInfo]) => activeInfo),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
);
|
||||
|
||||
activeTab$ = concat(
|
||||
defer(async () => {
|
||||
const currentTab = await BrowserApi.getTabFromCurrentWindow();
|
||||
if (currentTab == null || currentTab.id === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { tabId: currentTab.id, windowId: currentTab.windowId };
|
||||
}),
|
||||
activeTabsUpdated$ = concat(
|
||||
defer(async () => await this.getActiveTabs()),
|
||||
merge(
|
||||
this.onTabActivated$,
|
||||
this.onTabActivated$.pipe(
|
||||
switchMap(async (activeInfo) => {
|
||||
const tab = await BrowserApi.getTab(activeInfo.tabId);
|
||||
|
||||
if (tab == undefined || tab.id == undefined || tab.url == undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [tabFromChromeTab(tab)];
|
||||
}),
|
||||
),
|
||||
fromChromeEvent(chrome.tabs.onUpdated).pipe(
|
||||
filter(
|
||||
([_, changeInfo]) =>
|
||||
// Only emit if the url was updated
|
||||
changeInfo.url != undefined,
|
||||
),
|
||||
map(([tabId, _changeInfo, tab]) => ({ tabId, windowId: tab.windowId })),
|
||||
map(([_tabId, _changeInfo, tab]) => [tabFromChromeTab(tab)]),
|
||||
),
|
||||
),
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
||||
fromChromeEvent(chrome.webNavigation.onCommitted).pipe(
|
||||
map(([details]) => {
|
||||
const toReturn: Tab[] =
|
||||
details.transitionType === "reload" ? [{ tabId: details.tabId, url: details.url }] : [];
|
||||
return toReturn;
|
||||
}),
|
||||
),
|
||||
// NOTE: We're only sharing the active tab changes, not the full list of active tabs.
|
||||
// This is so that any new subscriber will get the latest active tabs immediately, but
|
||||
// doesn't re-subscribe to chrome events.
|
||||
).pipe(shareReplay({ bufferSize: 1, refCount: true })),
|
||||
).pipe(filter((tabs) => tabs.length > 0));
|
||||
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
return BrowserApi.getActiveTabs();
|
||||
async getActiveTabs(): Promise<Tab[]> {
|
||||
const tabs = await BrowserApi.getActiveTabs();
|
||||
return tabs.filter((tab) => tab.id != undefined && tab.url != undefined).map(tabFromChromeTab);
|
||||
}
|
||||
|
||||
constructor(private platformUtilsService: PlatformUtilsService) {}
|
||||
|
||||
@@ -39,7 +39,6 @@ describe("BadgeService", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = [tabId];
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -195,7 +194,6 @@ describe("BadgeService", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -240,7 +238,6 @@ describe("BadgeService", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTabs(activeTabIds);
|
||||
badgeApi.setLastActivatedTab(1);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -286,7 +283,6 @@ describe("BadgeService", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = [tabId];
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -562,7 +558,6 @@ describe("BadgeService", () => {
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTabs([tabId]);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
@@ -595,14 +590,12 @@ describe("BadgeService", () => {
|
||||
});
|
||||
|
||||
describe("given multiple tabs are open and multiple are active", () => {
|
||||
const tabId = 1;
|
||||
const activeTabIds = [1, 2];
|
||||
const tabIds = [1, 2, 3];
|
||||
|
||||
beforeEach(() => {
|
||||
badgeApi.tabs = tabIds;
|
||||
badgeApi.setActiveTabs(activeTabIds);
|
||||
badgeApi.setLastActivatedTab(tabId);
|
||||
badgeServiceSubscription = badgeService.startListening();
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
StateProvider,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
|
||||
import { BadgeBrowserApi, RawBadgeState } from "./badge-browser-api";
|
||||
import { BadgeBrowserApi, RawBadgeState, Tab } from "./badge-browser-api";
|
||||
import { DefaultBadgeState } from "./consts";
|
||||
import { BadgeStatePriority } from "./priority";
|
||||
import { BadgeState, Unset } from "./state";
|
||||
@@ -21,11 +21,23 @@ interface StateSetting {
|
||||
|
||||
const BADGE_STATES = new KeyDefinition(BADGE_MEMORY, "badgeStates", {
|
||||
deserializer: (value: Record<string, StateSetting>) => value ?? {},
|
||||
cleanupDelayMs: 0,
|
||||
});
|
||||
|
||||
export class BadgeService {
|
||||
private serviceState: GlobalState<Record<string, StateSetting>>;
|
||||
|
||||
/**
|
||||
* An observable that emits whenever one or multiple tabs are updated and might need its state updated.
|
||||
* Use this to know exactly which tabs to calculate the badge state for.
|
||||
* This is not the same as `onActivated` which only emits when the active tab changes.
|
||||
*/
|
||||
activeTabsUpdated$ = this.badgeApi.activeTabsUpdated$;
|
||||
|
||||
getActiveTabs(): Promise<Tab[]> {
|
||||
return this.badgeApi.getActiveTabs();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private badgeApi: BadgeBrowserApi,
|
||||
@@ -40,12 +52,12 @@ export class BadgeService {
|
||||
*/
|
||||
startListening(): Subscription {
|
||||
// React to tab changes
|
||||
return this.badgeApi.activeTab$
|
||||
return this.badgeApi.activeTabsUpdated$
|
||||
.pipe(
|
||||
withLatestFrom(this.serviceState.state$),
|
||||
filter(([activeTab]) => activeTab != undefined),
|
||||
concatMap(async ([activeTab, serviceState]) => {
|
||||
await this.updateBadge(serviceState, activeTab!.tabId);
|
||||
filter(([activeTabs]) => activeTabs.length > 0),
|
||||
concatMap(async ([activeTabs, serviceState]) => {
|
||||
await Promise.all(activeTabs.map((tab) => this.updateBadge(serviceState, tab.tabId)));
|
||||
}),
|
||||
)
|
||||
.subscribe({
|
||||
@@ -78,7 +90,6 @@ export class BadgeService {
|
||||
...s,
|
||||
[name]: { priority, state, tabId },
|
||||
}));
|
||||
|
||||
await this.updateBadge(newServiceState, tabId);
|
||||
}
|
||||
|
||||
@@ -104,7 +115,6 @@ export class BadgeService {
|
||||
if (clearedState === undefined) {
|
||||
return;
|
||||
}
|
||||
// const activeTabs = await firstValueFrom(this.badgeApi.activeTabs$);
|
||||
await this.updateBadge(newServiceState, clearedState.tabId);
|
||||
}
|
||||
|
||||
@@ -151,16 +161,15 @@ export class BadgeService {
|
||||
* @param tabId Tab id for which the the latest state change applied to. Set this to activeTab.tabId to force an update.
|
||||
*/
|
||||
private async updateBadge(
|
||||
// activeTabs: chrome.tabs.Tab[],
|
||||
serviceState: Record<string, StateSetting> | null | undefined,
|
||||
tabId: number | undefined,
|
||||
) {
|
||||
const activeTabs = await this.badgeApi.getActiveTabs();
|
||||
if (tabId !== undefined && !activeTabs.some((tab) => tab.id === tabId)) {
|
||||
if (tabId !== undefined && !activeTabs.some((tab) => tab.tabId === tabId)) {
|
||||
return; // No need to update the badge if the state is not for the active tab.
|
||||
}
|
||||
|
||||
const tabIdsToUpdate = tabId ? [tabId] : activeTabs.map((tab) => tab.id);
|
||||
const tabIdsToUpdate = tabId ? [tabId] : activeTabs.map((tab) => tab.tabId);
|
||||
|
||||
for (const tabId of tabIdsToUpdate) {
|
||||
if (tabId === undefined) {
|
||||
|
||||
@@ -1,38 +1,33 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { BadgeBrowserApi, RawBadgeState } from "../badge-browser-api";
|
||||
import { BadgeBrowserApi, RawBadgeState, Tab } from "../badge-browser-api";
|
||||
|
||||
export class MockBadgeBrowserApi implements BadgeBrowserApi {
|
||||
private _activeTab$ = new BehaviorSubject<chrome.tabs.TabActiveInfo | undefined>(undefined);
|
||||
activeTab$ = this._activeTab$.asObservable();
|
||||
private _activeTabsUpdated$ = new BehaviorSubject<Tab[]>([]);
|
||||
activeTabsUpdated$ = this._activeTabsUpdated$.asObservable();
|
||||
|
||||
specificStates: Record<number, RawBadgeState> = {};
|
||||
generalState?: RawBadgeState;
|
||||
tabs: number[] = [];
|
||||
activeTabs: number[] = [];
|
||||
|
||||
getActiveTabs(): Promise<chrome.tabs.Tab[]> {
|
||||
getActiveTabs(): Promise<Tab[]> {
|
||||
return Promise.resolve(
|
||||
this.activeTabs.map(
|
||||
(tabId) =>
|
||||
({
|
||||
id: tabId,
|
||||
windowId: 1,
|
||||
active: true,
|
||||
}) as chrome.tabs.Tab,
|
||||
tabId,
|
||||
url: `https://example.com/${tabId}`,
|
||||
}) satisfies Tab,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setActiveTabs(tabs: number[]) {
|
||||
this.activeTabs = tabs;
|
||||
}
|
||||
|
||||
setLastActivatedTab(tabId: number) {
|
||||
this._activeTab$.next({
|
||||
tabId,
|
||||
windowId: 1,
|
||||
});
|
||||
this._activeTabsUpdated$.next(
|
||||
tabs.map((tabId) => ({ tabId, url: `https://example.com/${tabId}` })),
|
||||
);
|
||||
}
|
||||
|
||||
setState = jest.fn().mockImplementation((state: RawBadgeState, tabId?: number): Promise<void> => {
|
||||
|
||||
Reference in New Issue
Block a user