diff --git a/packages/frontend/core/src/blocksuite/view-extensions/editor-config/linked.ts b/packages/frontend/core/src/blocksuite/view-extensions/editor-config/linked.ts index 33f12269b..8b6bf910c 100644 --- a/packages/frontend/core/src/blocksuite/view-extensions/editor-config/linked.ts +++ b/packages/frontend/core/src/blocksuite/view-extensions/editor-config/linked.ts @@ -1,17 +1,11 @@ -import { - AtMenuConfigService, - type LinkedMenuGroupType, -} from '@affine/core/modules/at-menu-config/services'; +import { AtMenuConfigService } from '@affine/core/modules/at-menu-config/services'; import type { LinkedWidgetConfig } from '@blocksuite/affine/widgets/linked-doc'; import { type FrameworkProvider } from '@toeverything/infra'; export function createLinkedWidgetConfig( - framework: FrameworkProvider, - options?: { - includedGroups?: LinkedMenuGroupType[]; - } + framework: FrameworkProvider ): Partial | undefined { const service = framework.getOptional(AtMenuConfigService); if (!service) return; - return service.getConfig(options?.includedGroups); + return service.getConfig(); } diff --git a/packages/frontend/core/src/components/comment/comment-editor/linked-widget-config.ts b/packages/frontend/core/src/components/comment/comment-editor/linked-widget-config.ts new file mode 100644 index 000000000..91417ce51 --- /dev/null +++ b/packages/frontend/core/src/components/comment/comment-editor/linked-widget-config.ts @@ -0,0 +1,165 @@ +import { MemberSearchService } from '@affine/core/modules/permissions'; +import { highlighter } from '@affine/core/modules/quicksearch/utils/highlighter'; +import { I18n } from '@affine/i18n'; +import track from '@affine/track'; +import type { AffineInlineEditor } from '@blocksuite/affine/shared/types'; +import type { + LinkedMenuItem, + LinkedWidgetConfig, +} from '@blocksuite/affine/widgets/linked-doc'; +import { UserIcon } from '@blocksuite/icons/lit'; +import { BLOCK_ID_ATTR, type BlockComponent } from '@blocksuite/std'; +import { computed } from '@preact/signals-core'; +import type { FrameworkProvider } from '@toeverything/infra'; +import { cssVarV2 } from '@toeverything/theme/v2'; +import Fuse, { type FuseResultMatch } from 'fuse.js'; +import { html } from 'lit'; +import { styleMap } from 'lit/directives/style-map.js'; +import { unsafeHTML } from 'lit/directives/unsafe-html.js'; + +export const createCommentLinkedWidgetConfig = ( + framework: FrameworkProvider +): Partial | undefined => { + const memberGroup = ( + query: string, + close: () => void, + inlineEditor: AffineInlineEditor + ) => { + const memberSearchService = framework.get(MemberSearchService); + + const getMenuItem = ( + id: string, + name?: string | null, + avatar?: string | null + ): LinkedMenuItem => { + const avatarStyle = styleMap({ + borderRadius: '50%', + border: `1px solid ${cssVarV2('layer/background/overlayPanel')}`, + width: '20px', + height: '20px', + boxSizing: 'border-box', + }); + const icon = avatar + ? html`` + : UserIcon(); + + let displayName = name ?? 'Unknown'; + return { + key: id, + name: html`${unsafeHTML(displayName)}`, + icon, + action: () => { + const root = inlineEditor.rootElement; + const block = root?.closest(`[${BLOCK_ID_ATTR}]`); + if (!block) return; + + close(); + + track.doc.editor.atMenu.mentionMember({ + type: 'member', + }); + + const inlineRange = inlineEditor.getInlineRange(); + if (!inlineRange || inlineRange.length !== 0) return; + + inlineEditor.insertText(inlineRange, ' ', { + mention: { + member: id, + }, + }); + inlineEditor.setInlineRange({ + index: inlineRange.index + 1, + length: 0, + }); + }, + }; + }; + const highlightFuseTitle = ( + matches: readonly FuseResultMatch[] | undefined, + title: string, + key: string + ): string => { + if (!matches) { + return title; + } + const normalizedRange = ([start, end]: [number, number]) => + [ + start, + end + 1 /* in fuse, the `end` is different from the `substring` */, + ] as [number, number]; + const titleMatches = matches + ?.filter(match => match.key === key) + .flatMap(match => match.indices.map(normalizedRange)); + return ( + highlighter( + title, + ``, + '', + titleMatches ?? [] + ) ?? title + ); + }; + + memberSearchService.search(query); + + const items = computed(() => { + const members = memberSearchService.result$.signal.value; + console.log('members', members); + + if (query.length === 0) { + return members + .slice(0, 3) + .map(member => getMenuItem(member.id, member.name, member.avatarUrl)); + } + + // Create a single Fuse instance for all members + const fuse = new Fuse(members, { + keys: ['name'], + includeMatches: true, + includeScore: true, + ignoreLocation: true, + threshold: 0.0, + }); + const searchResults = fuse.search(query); + + return searchResults.map(result => { + const member = result.item; + const displayName = highlightFuseTitle( + result.matches, + member.name ?? 'Unknown', + 'name' + ); + return { + ...getMenuItem(member.id, member.name, member.avatarUrl), + name: html`${unsafeHTML(displayName)}`, + }; + }); + }); + + return { + name: I18n.t('com.affine.editor.at-menu.mention-members'), + items, + loading: memberSearchService.isLoading$.signal, + hidden: computed(() => { + return ( + memberSearchService.result$.signal.value.length === 0 && + !memberSearchService.isLoading$.signal.value + ); + }), + maxDisplay: 3, + overflowText: computed(() => { + const totalCount = memberSearchService.result$.signal.value.length; + const remainingCount = totalCount - 3; + return I18n.t('com.affine.editor.at-menu.more-members-hint', { + count: remainingCount, + }); + }), + }; + }; + + return { + getMenus: (query, close, _editorHost, inlineEditor) => { + return [memberGroup(query, close, inlineEditor)]; + }, + }; +}; diff --git a/packages/frontend/core/src/components/comment/comment-editor/specs.ts b/packages/frontend/core/src/components/comment/comment-editor/specs.ts index 1db18e0e5..fb1ea2dc2 100644 --- a/packages/frontend/core/src/components/comment/comment-editor/specs.ts +++ b/packages/frontend/core/src/components/comment/comment-editor/specs.ts @@ -1,8 +1,6 @@ import { CloudViewExtension } from '@affine/core/blocksuite/view-extensions/cloud'; -import { createLinkedWidgetConfig } from '@affine/core/blocksuite/view-extensions/editor-config/linked'; import { AffineEditorViewExtension } from '@affine/core/blocksuite/view-extensions/editor-view/editor-view'; import { AffineThemeViewExtension } from '@affine/core/blocksuite/view-extensions/theme'; -import { LinkedMenuGroupType } from '@affine/core/modules/at-menu-config/services'; import { CodeBlockViewExtension } from '@blocksuite/affine/blocks/code/view'; import { DividerViewExtension } from '@blocksuite/affine/blocks/divider/view'; import { LatexViewExtension as LatexBlockViewExtension } from '@blocksuite/affine/blocks/latex/view'; @@ -58,6 +56,8 @@ import { ViewportOverlayViewExtension } from '@blocksuite/affine/widgets/viewpor import type { FrameworkProvider } from '@toeverything/infra'; import { z } from 'zod'; +import { createCommentLinkedWidgetConfig } from './linked-widget-config'; + const commentEditorViewExtensionOptionsSchema = z.object({ peekView: z.optional(z.custom()), fontConfig: z.optional(z.array(fontConfigSchema)), @@ -160,9 +160,7 @@ export function getCommentEditorViewManager(framework: FrameworkProvider) { manager.configure( LinkedDocViewExtension, - createLinkedWidgetConfig(framework, { - includedGroups: [LinkedMenuGroupType.Mention], - }) + createCommentLinkedWidgetConfig(framework) ); manager.configure(CloudViewExtension, { diff --git a/packages/frontend/core/src/modules/at-menu-config/services/index.ts b/packages/frontend/core/src/modules/at-menu-config/services/index.ts index 496024967..ec971e20f 100644 --- a/packages/frontend/core/src/modules/at-menu-config/services/index.ts +++ b/packages/frontend/core/src/modules/at-menu-config/services/index.ts @@ -62,13 +62,6 @@ const RESERVED_ITEM_KEYS = { datePicker: 'date-picker', }; -export enum LinkedMenuGroupType { - LinkToDoc = 'link-to-doc', - Mention = 'mention', - Journal = 'journal', - NewDoc = 'new-doc', -} - export class AtMenuConfigService extends Service { constructor( private readonly journalService: JournalService, @@ -86,11 +79,9 @@ export class AtMenuConfigService extends Service { // todo(@peng17): maybe refactor the config using entity, so that each config // can be reactive to the query, instead of recreating the whole config? - getConfig( - includedGroups?: LinkedMenuGroupType[] - ): Partial { + getConfig(): Partial { return { - getMenus: this.getMenusFn(includedGroups), + getMenus: this.getMenusFn(), mobile: this.getMobileConfig(), autoFocusedItemKey: this.autoFocusedItemKey, }; @@ -111,14 +102,14 @@ export class AtMenuConfigService extends Service { return null; } - const linkToDocGroup = menus.at(0); - const memberGroup = menus.at(1); + const linkToDocGroup = menus[0]; + const memberGroup = menus[1]; - if (memberGroup && resolveSignal(memberGroup.items).length > 1) { + if (resolveSignal(memberGroup.items).length > 1) { return resolveSignal(memberGroup.items)[0]?.key; } - if (linkToDocGroup && resolveSignal(linkToDocGroup.items).length > 0) { + if (resolveSignal(linkToDocGroup.items).length > 0) { return resolveSignal(linkToDocGroup.items)[0]?.key; } @@ -644,7 +635,9 @@ export class AtMenuConfigService extends Service { return query.length > 0 && !loading && members.length === 0; }); - this.memberSearchService.search(query); + if (query.length > 0) { + this.memberSearchService.search(query); + } return { name: I18n.t('com.affine.editor.at-menu.mention-members'), @@ -662,28 +655,13 @@ export class AtMenuConfigService extends Service { }; } - private getMenusFn( - includedGroups: LinkedMenuGroupType[] = [ - LinkedMenuGroupType.LinkToDoc, - LinkedMenuGroupType.Mention, - LinkedMenuGroupType.Journal, - LinkedMenuGroupType.NewDoc, - ] - ): LinkedWidgetConfig['getMenus'] { + private getMenusFn(): LinkedWidgetConfig['getMenus'] { return (query, close, editorHost, inlineEditor, abortSignal) => { return [ - ...(includedGroups?.includes(LinkedMenuGroupType.LinkToDoc) - ? [this.linkToDocGroup(query, close, inlineEditor, abortSignal)] - : []), - ...(includedGroups?.includes(LinkedMenuGroupType.Mention) - ? [this.memberGroup(query, close, inlineEditor, abortSignal)] - : []), - ...(includedGroups?.includes(LinkedMenuGroupType.Journal) - ? [this.journalGroup(query, close, inlineEditor)] - : []), - ...(includedGroups?.includes(LinkedMenuGroupType.NewDoc) - ? [this.newDocMenuGroup(query, close, editorHost, inlineEditor)] - : []), + this.linkToDocGroup(query, close, inlineEditor, abortSignal), + this.memberGroup(query, close, inlineEditor, abortSignal), + this.journalGroup(query, close, inlineEditor), + this.newDocMenuGroup(query, close, editorHost, inlineEditor), ]; }; }