feat(core): user service loading state (#10922)

This commit is contained in:
EYHN
2025-03-17 09:28:25 +00:00
parent 7dbc9b42b7
commit b401012d85
4 changed files with 70 additions and 8 deletions

View File

@@ -6,6 +6,8 @@ import type { AffineUserInfo } from './types';
export interface UserService { export interface UserService {
userInfo$(id: string): Signal<AffineUserInfo | null>; userInfo$(id: string): Signal<AffineUserInfo | null>;
isLoading$(id: string): Signal<boolean>;
error$(id: string): Signal<string | null>; // user friendly error string
revalidateUserInfo(id: string): void; revalidateUserInfo(id: string): void;
} }

View File

@@ -346,9 +346,16 @@ function createToolbarMoreMenuConfigV2(baseUrl?: string) {
const userProvider = cx.std.get(UserProvider); const userProvider = cx.std.get(UserProvider);
userProvider.revalidateUserInfo(createdByUserId); userProvider.revalidateUserInfo(createdByUserId);
const userSignal = userProvider.userInfo$(createdByUserId); const userSignal = userProvider.userInfo$(createdByUserId);
const isLoadingSignal = userProvider.isLoading$(createdByUserId);
const name = computed(() => { const name = computed(() => {
const value = userSignal.value; const value = userSignal.value;
if (!value) return I18n['Unknown User'](); if (!value) {
if (isLoadingSignal.value) {
// if user info is loading
return '';
}
return I18n['Unknown User']();
}
const removed = isRemovedUserInfo(value); const removed = isRemovedUserInfo(value);
if (removed) { if (removed) {
return I18n['Deleted User'](); return I18n['Deleted User']();

View File

@@ -1,4 +1,5 @@
import type { PublicUserService } from '@affine/core/modules/cloud'; import type { PublicUserService } from '@affine/core/modules/cloud';
import { UserFriendlyError } from '@affine/error';
import { UserServiceExtension } from '@blocksuite/affine/shared/services'; import { UserServiceExtension } from '@blocksuite/affine/shared/services';
export function patchUserExtensions(publicUserService: PublicUserService) { export function patchUserExtensions(publicUserService: PublicUserService) {
@@ -7,6 +8,20 @@ export function patchUserExtensions(publicUserService: PublicUserService) {
userInfo$(id) { userInfo$(id) {
return publicUserService.publicUser$(id).signal; return publicUserService.publicUser$(id).signal;
}, },
// eslint-disable-next-line rxjs/finnish
isLoading$(id) {
return publicUserService.isLoading$(id).signal;
},
// eslint-disable-next-line rxjs/finnish
error$(id) {
return publicUserService.error$(id).selector(error => {
if (error) {
return UserFriendlyError.fromAny(error).name;
} else {
return null;
}
}).signal;
},
revalidateUserInfo(id) { revalidateUserInfo(id) {
publicUserService.revalidate(id); publicUserService.revalidate(id);
}, },

View File

@@ -2,6 +2,8 @@ import {
effect, effect,
fromPromise, fromPromise,
LiveData, LiveData,
onComplete,
onStart,
Service, Service,
smartRetry, smartRetry,
} from '@toeverything/infra'; } from '@toeverything/infra';
@@ -28,13 +30,48 @@ export class PublicUserService extends Service {
super(); super();
} }
publicUsers$ = new LiveData<Map<string, PublicUserInfo | null>>(new Map()); private readonly publicUsers$ = new LiveData<
Map<string, PublicUserInfo | null>
>(new Map());
private readonly isLoadings$ = new LiveData<Map<string, boolean | null>>(
new Map()
);
private readonly errors$ = new LiveData<Map<string, any | null>>(new Map());
publicUser$(id: string) { publicUser$(id: string) {
return this.publicUsers$.selector(map => map.get(id) ?? null); return this.publicUsers$.selector(map => map.get(id) ?? null);
} }
error$ = new LiveData<any | null>(null); isLoading$(id: string) {
return this.isLoadings$.selector(map => map.get(id) ?? false);
}
error$(id: string) {
return this.errors$.selector(map => map.get(id));
}
private setPublicUser(id: string, userInfo: PublicUserInfo) {
// Reusing the existing publicUsers Map instance instead of creating a new one.
// While this doesn't follow immutability best practices, it reduces memory overhead
// by avoiding the creation of new Map objects for each update.
const publicUsers = this.publicUsers$.value;
publicUsers.set(id, userInfo);
this.publicUsers$.next(publicUsers);
}
private setLoading(id: string, loading: boolean) {
// Similar to setPublicUser, reusing the existing Map instance to reduce memory overhead
const loadings = this.isLoadings$.value;
loadings.set(id, loading);
this.isLoadings$.next(loadings);
}
private setError(id: string, error: any | null) {
// Reusing the existing Map instance instead of creating a new one
const errors = this.errors$.value;
errors.set(id, error);
this.errors$.next(errors);
}
revalidate = effect( revalidate = effect(
groupBy((id: string) => id), groupBy((id: string) => id),
@@ -58,15 +95,16 @@ export class PublicUserService extends Service {
smartRetry(), smartRetry(),
catchError(error => { catchError(error => {
console.error(error); console.error(error);
this.error$.next(error); this.setError(id, error);
return EMPTY; return EMPTY;
}), }),
mergeMap(user => { mergeMap(user => {
const publicUsers = new Map(this.publicUsers$.value); this.setPublicUser(id, user);
publicUsers.set(user.id, user); this.setError(id, null); // clear error
this.publicUsers$.next(publicUsers);
return EMPTY; return EMPTY;
}) }),
onStart(() => this.setLoading(id, true)),
onComplete(() => this.setLoading(id, false))
) )
) )
) )