feat(core): add user list service for blocksuite (#10627)

This commit is contained in:
EYHN
2025-03-05 10:06:13 +00:00
parent 47d01f5f66
commit 201c3438ba
9 changed files with 73 additions and 33 deletions
@@ -5,8 +5,8 @@ export type RemovedUserInfo = {
export type ExistedUserInfo = {
id: string;
name: string;
avatar: string;
name?: string | null;
avatar?: string | null;
removed?: false;
};
+2
View File
@@ -255,6 +255,8 @@ export default tseslint.config(
types: {
'^LiveData$': true,
'^Signal$': true,
'^ReadonlySignal$': true,
'^Doc$': false,
'^Awareness$': false,
'^UndoManager$': false,
+1
View File
@@ -15,6 +15,7 @@
"@affine/env": "workspace:*",
"@affine/templates": "workspace:*",
"@datastructures-js/binary-search-tree": "^5.3.2",
"@preact/signals-core": "^1.8.0",
"eventemitter2": "^6.4.9",
"foxact": "^0.2.43",
"fractional-indexing": "^3.2.0",
+26 -2
View File
@@ -1,4 +1,5 @@
import { DebugLogger } from '@affine/debug';
import { type ReadonlySignal, type Signal, signal } from '@preact/signals-core';
import type {
InteropObservable,
Observer,
@@ -140,6 +141,17 @@ export class LiveData<T = unknown>
return data$;
}
static fromSignal<T>(signal: ReadonlySignal<T>): LiveData<T> {
return LiveData.from(
new Observable(subscriber => {
signal.subscribe(value => {
subscriber.next(value);
});
}),
signal.value
);
}
private static GLOBAL_COMPUTED_RECURSIVE_COUNT = 0;
/**
@@ -286,6 +298,19 @@ export class LiveData<T = unknown>
this.next(v);
}
private _signal: Signal<T> | undefined;
get signal(): ReadonlySignal<T> {
if (!this._signal) {
this._signal = signal(this.value);
this.subscribe(v => {
// oxlint-disable-next-line no-non-null-assertion
this._signal!.value = v;
});
}
return this._signal;
}
next = (v: T) => {
if (this.isPoisoned) {
throw this.poisonedError;
@@ -379,7 +404,6 @@ export class LiveData<T = unknown>
);
}
// eslint-disable-next-line rxjs/finnish
asObservable(): Observable<T> {
return new Observable<T>(subscriber => {
return this.subscribe(subscriber);
@@ -421,7 +445,7 @@ export class LiveData<T = unknown>
override pipe(...args: any[]) {
return new Observable(subscriber => {
this.ops$.next('watch');
// eslint-disable-next-line prefer-spread
const subscription = this.raw$.pipe
.apply(this.raw$, args as any)
.subscribe(subscriber);
@@ -19,6 +19,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { JournalService } from '@affine/core/modules/journal';
import { toURLSearchParams } from '@affine/core/modules/navigation';
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
import { MemberSearchService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import track from '@affine/track';
import {
@@ -69,6 +70,7 @@ import {
type ReferenceReactRenderer,
} from '../extensions/reference-renderer';
import { patchSideBarService } from '../extensions/side-bar-service';
import { patchUserListExtensions } from '../extensions/user-list';
import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
import { StarterBar } from './starter-bar';
@@ -90,6 +92,7 @@ const usePatchSpecs = (mode: DocMode) => {
editorService,
workspaceService,
featureFlagService,
memberSearchService,
} = useServices({
PeekViewService,
DocService,
@@ -97,6 +100,7 @@ const usePatchSpecs = (mode: DocMode) => {
WorkspaceService,
EditorService,
FeatureFlagService,
MemberSearchService,
});
const framework = useFramework();
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
@@ -151,6 +155,7 @@ const usePatchSpecs = (mode: DocMode) => {
patchPeekViewService(peekViewService),
patchOpenDocExtension(),
EdgelessClipboardWatcher,
patchUserListExtensions(memberSearchService),
patchDocUrlExtensions(framework),
patchQuickSearchService(framework),
patchSideBarService(framework),
@@ -173,18 +178,19 @@ const usePatchSpecs = (mode: DocMode) => {
return builder.value;
}, [
framework,
mode,
enableAI,
reactToLit,
referenceRenderer,
confirmModal,
peekViewService,
memberSearchService,
docService,
docsService,
editorService,
framework,
peekViewService,
reactToLit,
referenceRenderer,
featureFlagService,
enableAI,
enableTurboRenderer,
featureFlagService.flags.enable_pdf_embed_preview.value,
]);
return [
@@ -0,0 +1,23 @@
import type { MemberSearchService } from '@affine/core/modules/permissions';
import { UserListServiceExtension } from '@blocksuite/affine/blocks';
export function patchUserListExtensions(memberSearch: MemberSearchService) {
return UserListServiceExtension({
// eslint-disable-next-line rxjs/finnish
hasMore$: memberSearch.hasMore$.signal,
loadMore() {
memberSearch.loadMore();
},
search(keyword) {
memberSearch.search(keyword);
},
// eslint-disable-next-line rxjs/finnish
users$: memberSearch.result$.map(users =>
users.map(u => ({
id: u.id,
name: u.name,
avatar: u.avatarUrl,
}))
).signal,
});
}
@@ -2,14 +2,14 @@ import {
DatabaseBlockDataSource,
type DatabaseBlockModel,
} from '@blocksuite/affine/blocks';
import { Service } from '@toeverything/infra';
import { LiveData, Service } from '@toeverything/infra';
import { isEqual } from 'lodash-es';
import { combineLatest, distinctUntilChanged, map, Observable } from 'rxjs';
import type { DocsService } from '../../doc';
import type { DocsSearchService } from '../../docs-search';
import type { DatabaseRow, DatabaseValueCell } from '../types';
import { signalToLiveData, signalToObservable } from '../utils';
import { signalToObservable } from '../utils';
const equalComparator = <T>(a: T, b: T) => {
return isEqual(a, b);
@@ -50,14 +50,14 @@ export class DocDatabaseBacklinksService extends Service {
.map<DatabaseValueCell>(id => {
return {
id,
value$: signalToLiveData(
value$: LiveData.fromSignal(
dataSource.cellValueGet$(rowId, id)
).distinctUntilChanged(equalComparator),
property: {
id,
type$: signalToLiveData(dataSource.propertyTypeGet$(id)),
name$: signalToLiveData(dataSource.propertyNameGet$(id)),
data$: signalToLiveData(dataSource.propertyDataGet$(id)),
type$: LiveData.fromSignal(dataSource.propertyTypeGet$(id)),
name$: LiveData.fromSignal(dataSource.propertyNameGet$(id)),
data$: LiveData.fromSignal(dataSource.propertyDataGet$(id)),
},
};
})
@@ -2,7 +2,6 @@ import { DebugLogger } from '@affine/debug';
import { BlockStdScope } from '@blocksuite/affine/block-std';
import { PageEditorBlockSpecs } from '@blocksuite/affine/blocks';
import type { Store } from '@blocksuite/affine/store';
import { LiveData } from '@toeverything/infra';
import { useMemo } from 'react';
import { Observable } from 'rxjs';
@@ -25,22 +24,6 @@ export function signalToObservable<T>(
});
}
export function signalToLiveData<T>(
signal: ReadonlySignal<T>,
defaultValue: T
): LiveData<T>;
export function signalToLiveData<T>(
signal: ReadonlySignal<T>
): LiveData<T | undefined>;
export function signalToLiveData<T>(
signal: ReadonlySignal<T>,
defaultValue?: T
) {
return LiveData.from(signalToObservable(signal), defaultValue);
}
// todo(pengx17): use rc pool?
export function createBlockStdScope(doc: Store) {
logger.debug('createBlockStdScope', doc.id);
+1
View File
@@ -13431,6 +13431,7 @@ __metadata:
"@affine/templates": "workspace:*"
"@datastructures-js/binary-search-tree": "npm:^5.3.2"
"@emotion/react": "npm:^11.14.0"
"@preact/signals-core": "npm:^1.8.0"
"@swc/core": "npm:^1.10.1"
"@testing-library/dom": "npm:^10.4.0"
"@testing-library/react": "npm:^16.1.0"