1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +00:00

[PM-18946] Improve Vault loading experience (#13714)

* [PM-18946] Refactor loading$ in vault-v2. Update icon-component, and build-cipher-icon
This commit is contained in:
Shane Melton
2025-03-13 11:38:29 -07:00
committed by GitHub
parent 81335978d8
commit 4687120618
10 changed files with 88 additions and 61 deletions

View File

@@ -2,16 +2,18 @@
<ng-container *ngIf="data$ | async as data">
<img
[src]="data.image"
[appFallbackSrc]="data.fallbackImage"
*ngIf="data.imageEnabled && data.image"
class="tw-size-6 tw-rounded-md"
alt=""
decoding="async"
loading="lazy"
[ngClass]="{ 'tw-invisible tw-absolute': !imageLoaded() }"
(load)="imageLoaded.set(true)"
(error)="imageLoaded.set(false)"
/>
<i
class="tw-w-6 tw-text-muted bwi bwi-lg {{ data.icon }}"
*ngIf="!data.imageEnabled || !data.image"
*ngIf="!data.imageEnabled || !data.image || !imageLoaded()"
></i>
</ng-container>
</div>

View File

@@ -1,18 +1,18 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectionStrategy, Component, Input, OnInit } from "@angular/core";
import { ChangeDetectionStrategy, Component, input, signal } from "@angular/core";
import { toObservable } from "@angular/core/rxjs-interop";
import {
BehaviorSubject,
combineLatest,
distinctUntilChanged,
filter,
map,
tap,
Observable,
startWith,
pairwise,
} from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { buildCipherIcon, CipherIconDetails } from "@bitwarden/common/vault/icon/build-cipher-icon";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
@@ -20,33 +20,40 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
templateUrl: "icon.component.html",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconComponent implements OnInit {
@Input()
set cipher(value: CipherView) {
this.cipher$.next(value);
}
export class IconComponent {
/**
* The cipher to display the icon for.
*/
cipher = input.required<CipherView>();
protected data$: Observable<{
imageEnabled: boolean;
image?: string;
fallbackImage: string;
icon?: string;
}>;
imageLoaded = signal(false);
private cipher$ = new BehaviorSubject<CipherView>(undefined);
protected data$: Observable<CipherIconDetails>;
constructor(
private environmentService: EnvironmentService,
private domainSettingsService: DomainSettingsService,
) {}
async ngOnInit() {
this.data$ = combineLatest([
) {
const iconSettings$ = combineLatest([
this.environmentService.environment$.pipe(map((e) => e.getIconsUrl())),
this.domainSettingsService.showFavicons$.pipe(distinctUntilChanged()),
this.cipher$.pipe(filter((c) => c !== undefined)),
]).pipe(
map(([iconsUrl, showFavicon, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
map(([iconsUrl, showFavicon]) => ({ iconsUrl, showFavicon })),
startWith({ iconsUrl: null, showFavicon: false }), // Start with a safe default to avoid flickering icons
distinctUntilChanged(),
);
this.data$ = combineLatest([iconSettings$, toObservable(this.cipher)]).pipe(
map(([{ iconsUrl, showFavicon }, cipher]) => buildCipherIcon(iconsUrl, cipher, showFavicon)),
startWith(null),
pairwise(),
tap(([prev, next]) => {
if (prev?.image !== next?.image) {
// The image changed, reset the loaded state to not show an empty icon
this.imageLoaded.set(false);
}
}),
map(([_, next]) => next!),
);
}
}