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

View File

@@ -5,8 +5,8 @@ export type RemovedUserInfo = {
export type ExistedUserInfo = { export type ExistedUserInfo = {
id: string; id: string;
name: string; name?: string | null;
avatar: string; avatar?: string | null;
removed?: false; removed?: false;
}; };

View File

@@ -255,6 +255,8 @@ export default tseslint.config(
types: { types: {
'^LiveData$': true, '^LiveData$': true,
'^Signal$': true,
'^ReadonlySignal$': true,
'^Doc$': false, '^Doc$': false,
'^Awareness$': false, '^Awareness$': false,
'^UndoManager$': false, '^UndoManager$': false,

View File

@@ -15,6 +15,7 @@
"@affine/env": "workspace:*", "@affine/env": "workspace:*",
"@affine/templates": "workspace:*", "@affine/templates": "workspace:*",
"@datastructures-js/binary-search-tree": "^5.3.2", "@datastructures-js/binary-search-tree": "^5.3.2",
"@preact/signals-core": "^1.8.0",
"eventemitter2": "^6.4.9", "eventemitter2": "^6.4.9",
"foxact": "^0.2.43", "foxact": "^0.2.43",
"fractional-indexing": "^3.2.0", "fractional-indexing": "^3.2.0",

View File

@@ -1,4 +1,5 @@
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { type ReadonlySignal, type Signal, signal } from '@preact/signals-core';
import type { import type {
InteropObservable, InteropObservable,
Observer, Observer,
@@ -140,6 +141,17 @@ export class LiveData<T = unknown>
return data$; 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; private static GLOBAL_COMPUTED_RECURSIVE_COUNT = 0;
/** /**
@@ -286,6 +298,19 @@ export class LiveData<T = unknown>
this.next(v); 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) => { next = (v: T) => {
if (this.isPoisoned) { if (this.isPoisoned) {
throw this.poisonedError; throw this.poisonedError;
@@ -379,7 +404,6 @@ export class LiveData<T = unknown>
); );
} }
// eslint-disable-next-line rxjs/finnish
asObservable(): Observable<T> { asObservable(): Observable<T> {
return new Observable<T>(subscriber => { return new Observable<T>(subscriber => {
return this.subscribe(subscriber); return this.subscribe(subscriber);
@@ -421,7 +445,7 @@ export class LiveData<T = unknown>
override pipe(...args: any[]) { override pipe(...args: any[]) {
return new Observable(subscriber => { return new Observable(subscriber => {
this.ops$.next('watch'); this.ops$.next('watch');
// eslint-disable-next-line prefer-spread
const subscription = this.raw$.pipe const subscription = this.raw$.pipe
.apply(this.raw$, args as any) .apply(this.raw$, args as any)
.subscribe(subscriber); .subscribe(subscriber);

View File

@@ -19,6 +19,7 @@ import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { JournalService } from '@affine/core/modules/journal'; import { JournalService } from '@affine/core/modules/journal';
import { toURLSearchParams } from '@affine/core/modules/navigation'; import { toURLSearchParams } from '@affine/core/modules/navigation';
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view'; 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 { WorkspaceService } from '@affine/core/modules/workspace';
import track from '@affine/track'; import track from '@affine/track';
import { import {
@@ -69,6 +70,7 @@ import {
type ReferenceReactRenderer, type ReferenceReactRenderer,
} from '../extensions/reference-renderer'; } from '../extensions/reference-renderer';
import { patchSideBarService } from '../extensions/side-bar-service'; import { patchSideBarService } from '../extensions/side-bar-service';
import { patchUserListExtensions } from '../extensions/user-list';
import { BiDirectionalLinkPanel } from './bi-directional-link-panel'; import { BiDirectionalLinkPanel } from './bi-directional-link-panel';
import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title'; import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title';
import { StarterBar } from './starter-bar'; import { StarterBar } from './starter-bar';
@@ -90,6 +92,7 @@ const usePatchSpecs = (mode: DocMode) => {
editorService, editorService,
workspaceService, workspaceService,
featureFlagService, featureFlagService,
memberSearchService,
} = useServices({ } = useServices({
PeekViewService, PeekViewService,
DocService, DocService,
@@ -97,6 +100,7 @@ const usePatchSpecs = (mode: DocMode) => {
WorkspaceService, WorkspaceService,
EditorService, EditorService,
FeatureFlagService, FeatureFlagService,
MemberSearchService,
}); });
const framework = useFramework(); const framework = useFramework();
const referenceRenderer: ReferenceReactRenderer = useMemo(() => { const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
@@ -151,6 +155,7 @@ const usePatchSpecs = (mode: DocMode) => {
patchPeekViewService(peekViewService), patchPeekViewService(peekViewService),
patchOpenDocExtension(), patchOpenDocExtension(),
EdgelessClipboardWatcher, EdgelessClipboardWatcher,
patchUserListExtensions(memberSearchService),
patchDocUrlExtensions(framework), patchDocUrlExtensions(framework),
patchQuickSearchService(framework), patchQuickSearchService(framework),
patchSideBarService(framework), patchSideBarService(framework),
@@ -173,18 +178,19 @@ const usePatchSpecs = (mode: DocMode) => {
return builder.value; return builder.value;
}, [ }, [
framework,
mode, mode,
enableAI,
reactToLit,
referenceRenderer,
confirmModal, confirmModal,
peekViewService,
memberSearchService,
docService, docService,
docsService, docsService,
editorService, editorService,
framework,
peekViewService,
reactToLit,
referenceRenderer,
featureFlagService,
enableAI,
enableTurboRenderer, enableTurboRenderer,
featureFlagService.flags.enable_pdf_embed_preview.value,
]); ]);
return [ return [

View File

@@ -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,
});
}

View File

@@ -2,14 +2,14 @@ import {
DatabaseBlockDataSource, DatabaseBlockDataSource,
type DatabaseBlockModel, type DatabaseBlockModel,
} from '@blocksuite/affine/blocks'; } from '@blocksuite/affine/blocks';
import { Service } from '@toeverything/infra'; import { LiveData, Service } from '@toeverything/infra';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { combineLatest, distinctUntilChanged, map, Observable } from 'rxjs'; import { combineLatest, distinctUntilChanged, map, Observable } from 'rxjs';
import type { DocsService } from '../../doc'; import type { DocsService } from '../../doc';
import type { DocsSearchService } from '../../docs-search'; import type { DocsSearchService } from '../../docs-search';
import type { DatabaseRow, DatabaseValueCell } from '../types'; import type { DatabaseRow, DatabaseValueCell } from '../types';
import { signalToLiveData, signalToObservable } from '../utils'; import { signalToObservable } from '../utils';
const equalComparator = <T>(a: T, b: T) => { const equalComparator = <T>(a: T, b: T) => {
return isEqual(a, b); return isEqual(a, b);
@@ -50,14 +50,14 @@ export class DocDatabaseBacklinksService extends Service {
.map<DatabaseValueCell>(id => { .map<DatabaseValueCell>(id => {
return { return {
id, id,
value$: signalToLiveData( value$: LiveData.fromSignal(
dataSource.cellValueGet$(rowId, id) dataSource.cellValueGet$(rowId, id)
).distinctUntilChanged(equalComparator), ).distinctUntilChanged(equalComparator),
property: { property: {
id, id,
type$: signalToLiveData(dataSource.propertyTypeGet$(id)), type$: LiveData.fromSignal(dataSource.propertyTypeGet$(id)),
name$: signalToLiveData(dataSource.propertyNameGet$(id)), name$: LiveData.fromSignal(dataSource.propertyNameGet$(id)),
data$: signalToLiveData(dataSource.propertyDataGet$(id)), data$: LiveData.fromSignal(dataSource.propertyDataGet$(id)),
}, },
}; };
}) })

View File

@@ -2,7 +2,6 @@ import { DebugLogger } from '@affine/debug';
import { BlockStdScope } from '@blocksuite/affine/block-std'; import { BlockStdScope } from '@blocksuite/affine/block-std';
import { PageEditorBlockSpecs } from '@blocksuite/affine/blocks'; import { PageEditorBlockSpecs } from '@blocksuite/affine/blocks';
import type { Store } from '@blocksuite/affine/store'; import type { Store } from '@blocksuite/affine/store';
import { LiveData } from '@toeverything/infra';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Observable } from 'rxjs'; 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? // todo(pengx17): use rc pool?
export function createBlockStdScope(doc: Store) { export function createBlockStdScope(doc: Store) {
logger.debug('createBlockStdScope', doc.id); logger.debug('createBlockStdScope', doc.id);

View File

@@ -13431,6 +13431,7 @@ __metadata:
"@affine/templates": "workspace:*" "@affine/templates": "workspace:*"
"@datastructures-js/binary-search-tree": "npm:^5.3.2" "@datastructures-js/binary-search-tree": "npm:^5.3.2"
"@emotion/react": "npm:^11.14.0" "@emotion/react": "npm:^11.14.0"
"@preact/signals-core": "npm:^1.8.0"
"@swc/core": "npm:^1.10.1" "@swc/core": "npm:^1.10.1"
"@testing-library/dom": "npm:^10.4.0" "@testing-library/dom": "npm:^10.4.0"
"@testing-library/react": "npm:^16.1.0" "@testing-library/react": "npm:^16.1.0"