From 201c3438ba968a41a4d86bfdd15a236cef6acd0a Mon Sep 17 00:00:00 2001 From: EYHN Date: Wed, 5 Mar 2025 10:06:13 +0000 Subject: [PATCH] feat(core): add user list service for blocksuite (#10627) --- .../shared/src/services/user-service/types.ts | 4 +-- eslint.config.mjs | 2 ++ packages/common/infra/package.json | 1 + .../common/infra/src/livedata/livedata.ts | 28 +++++++++++++++++-- .../block-suite-editor/lit-adaper.tsx | 18 ++++++++---- .../src/blocksuite/extensions/user-list.ts | 23 +++++++++++++++ .../services/doc-database-backlinks.ts | 12 ++++---- .../core/src/modules/doc-info/utils.ts | 17 ----------- yarn.lock | 1 + 9 files changed, 73 insertions(+), 33 deletions(-) create mode 100644 packages/frontend/core/src/blocksuite/extensions/user-list.ts diff --git a/blocksuite/affine/shared/src/services/user-service/types.ts b/blocksuite/affine/shared/src/services/user-service/types.ts index 5096689a2..2f670db6f 100644 --- a/blocksuite/affine/shared/src/services/user-service/types.ts +++ b/blocksuite/affine/shared/src/services/user-service/types.ts @@ -5,8 +5,8 @@ export type RemovedUserInfo = { export type ExistedUserInfo = { id: string; - name: string; - avatar: string; + name?: string | null; + avatar?: string | null; removed?: false; }; diff --git a/eslint.config.mjs b/eslint.config.mjs index d252525d6..fb1deee2f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -255,6 +255,8 @@ export default tseslint.config( types: { '^LiveData$': true, + '^Signal$': true, + '^ReadonlySignal$': true, '^Doc$': false, '^Awareness$': false, '^UndoManager$': false, diff --git a/packages/common/infra/package.json b/packages/common/infra/package.json index 264e607d2..a6b41726f 100644 --- a/packages/common/infra/package.json +++ b/packages/common/infra/package.json @@ -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", diff --git a/packages/common/infra/src/livedata/livedata.ts b/packages/common/infra/src/livedata/livedata.ts index bc3c0dd02..16de3a934 100644 --- a/packages/common/infra/src/livedata/livedata.ts +++ b/packages/common/infra/src/livedata/livedata.ts @@ -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 return data$; } + static fromSignal(signal: ReadonlySignal): LiveData { + 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 this.next(v); } + private _signal: Signal | undefined; + + get signal(): ReadonlySignal { + 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 ); } - // eslint-disable-next-line rxjs/finnish asObservable(): Observable { return new Observable(subscriber => { return this.subscribe(subscriber); @@ -421,7 +445,7 @@ export class LiveData 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); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx index e6d386d65..ba7f903aa 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/lit-adaper.tsx @@ -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 [ diff --git a/packages/frontend/core/src/blocksuite/extensions/user-list.ts b/packages/frontend/core/src/blocksuite/extensions/user-list.ts new file mode 100644 index 000000000..af468eb36 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/user-list.ts @@ -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, + }); +} diff --git a/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts b/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts index a14cb7e55..1fc59d01f 100644 --- a/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts +++ b/packages/frontend/core/src/modules/doc-info/services/doc-database-backlinks.ts @@ -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 = (a: T, b: T) => { return isEqual(a, b); @@ -50,14 +50,14 @@ export class DocDatabaseBacklinksService extends Service { .map(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)), }, }; }) diff --git a/packages/frontend/core/src/modules/doc-info/utils.ts b/packages/frontend/core/src/modules/doc-info/utils.ts index 49298310d..25e592b37 100644 --- a/packages/frontend/core/src/modules/doc-info/utils.ts +++ b/packages/frontend/core/src/modules/doc-info/utils.ts @@ -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( }); } -export function signalToLiveData( - signal: ReadonlySignal, - defaultValue: T -): LiveData; - -export function signalToLiveData( - signal: ReadonlySignal -): LiveData; - -export function signalToLiveData( - signal: ReadonlySignal, - defaultValue?: T -) { - return LiveData.from(signalToObservable(signal), defaultValue); -} - // todo(pengx17): use rc pool? export function createBlockStdScope(doc: Store) { logger.debug('createBlockStdScope', doc.id); diff --git a/yarn.lock b/yarn.lock index bb81c8fd1..cfa6a3d94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"