diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 321d7936806..9220091b0e6 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -3,6 +3,7 @@ import { toObservable } from "@angular/core/rxjs-interop"; import { combineLatest, concatMap, + debounceTime, distinctUntilChanged, distinctUntilKeyChanged, filter, @@ -99,6 +100,7 @@ export class VaultPopupItemsService { ...(showIdentities ? [CipherType.Identity] : []), ]; }), + distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])), ); /** @@ -111,6 +113,7 @@ export class VaultPopupItemsService { filter((userId): userId is UserId => userId != null), switchMap((userId) => merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe( + debounceTime(0), runInsideAngular(this.ngZone), tap(() => this._ciphersLoading$.next()), waitUntilSync(this.syncService), @@ -164,24 +167,35 @@ export class VaultPopupItemsService { }), ), ), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + + /** + * Observable that emits the search text when it's searchable, or an empty string when it's not. + * This prevents unnecessary re-renders when typing non-searchable text (e.g., single characters). + * @private + */ + private _effectiveSearchText$ = combineLatest([ + this.searchText$, + getUserId(this.accountService.activeAccount$), + ]).pipe( + switchMap(async ([searchText, userId]) => { + const isSearchable = await this.searchService.isSearchable(userId, searchText); + return isSearchable ? searchText : ""; + }), + distinctUntilChanged(), + shareReplay({ refCount: true, bufferSize: 1 }), ); /** * Observable that indicates whether there is search text present that is searchable. * @private */ - private _hasSearchText = combineLatest([ - this.searchText$, - getUserId(this.accountService.activeAccount$), - ]).pipe( - switchMap(([searchText, userId]) => { - return this.searchService.isSearchable(userId, searchText); - }), - ); + private _hasSearchText = this._effectiveSearchText$.pipe(map((text) => text !== "")); private _filteredCipherList$: Observable = combineLatest([ this._activeCipherList$, - this.searchText$, + this._effectiveSearchText$, this.vaultPopupListFiltersService.filterFunction$, getUserId(this.accountService.activeAccount$), ]).pipe( diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 85c415d01fe..fec625f34e2 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -100,6 +100,9 @@ export class VaultPopupListFiltersService { */ filters$ = this.filterForm.valueChanges.pipe( startWith(this.filterForm.value), + distinctUntilChanged( + (previous, current) => JSON.stringify(previous) === JSON.stringify(current), + ), shareReplay({ bufferSize: 1, refCount: true }), );