From 41d404f7f8328312d2c193181642a670f72e5682 Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Thu, 1 May 2025 14:29:11 +0000 Subject: [PATCH] refactor(editor): improve implementation of lit adapter (#12101) ## Summary by CodeRabbit - **New Features** - Improved mobile experience by disabling certain toolbars and slash menu features on mobile devices. - Introduced new modular extension classes for editor and view customization, enabling more flexible configuration of themes, AI features, and editor enhancements. - Added clipboard adapter configurations for a wide range of data types, improving clipboard compatibility. - Added a new theme extension specifically for preview scenarios. - Provided new hooks for block scope management in document modules. - **Refactor** - Streamlined editor extension setup, consolidating options and reducing complexity for better maintainability. - Reorganized mobile-specific extension exports for clearer usage. - Refined React-to-Lit rendering API by introducing a typed alias and updating related function signatures. - Simplified extension registration by splitting monolithic view extension into separate common and editor view extensions. - **Bug Fixes** - Corrected naming inconsistencies in internal effect tracking. - **Chores** - Updated type exports and documentation comments for improved clarity and consistency. - Removed unused or redundant exports and functions to clean up the codebase. --- blocksuite/affine/blocks/code/src/view.ts | 4 +- .../src/__tests__/ext-loader.unit.spec.ts | 12 +- .../affine/ext-loader/src/view-provider.ts | 26 ++- blocksuite/affine/foundation/src/clipboard.ts | 65 ++++++ blocksuite/affine/foundation/src/view.ts | 66 +------ .../shared/src/services/theme-service.ts | 12 +- .../affine/widgets/slash-menu/src/view.ts | 1 + blocksuite/affine/widgets/toolbar/src/view.ts | 1 + .../src/lit-react/lit-portal/index.ts | 1 + .../src/lit-react/lit-portal/lit-portal.tsx | 97 ++++----- .../block-suite-editor/lit-adaper.tsx | 157 ++------------- .../extensions/attachment-embed-view.tsx | 17 +- .../edgeless-block-header/index.tsx | 10 +- .../extensions/entry/enable-editor.ts | 13 +- .../extensions/entry/enable-preview.ts | 73 ------- .../core/src/blocksuite/extensions/index.ts | 1 - .../enable-mobile.ts => mobile-config.ts} | 42 +--- .../extensions/reference-renderer.ts | 7 +- .../core/src/blocksuite/extensions/theme.ts | 61 +++++- .../blocksuite/extensions/turbo-renderer.ts | 32 ++- .../src/blocksuite/manager/common-view.ts | 141 +++++++++++++ .../src/blocksuite/manager/editor-view.tsx | 187 ++++++++++++++++++ .../src/blocksuite/manager/migrating-view.ts | 143 ++------------ .../core/src/modules/doc-info/use-std.ts | 20 ++ .../core/src/modules/doc-info/utils.ts | 22 +-- .../database-properties/cells/rich-text.tsx | 2 +- 26 files changed, 638 insertions(+), 575 deletions(-) create mode 100644 blocksuite/affine/foundation/src/clipboard.ts delete mode 100644 packages/frontend/core/src/blocksuite/extensions/entry/enable-preview.ts delete mode 100644 packages/frontend/core/src/blocksuite/extensions/index.ts rename packages/frontend/core/src/blocksuite/extensions/{entry/enable-mobile.ts => mobile-config.ts} (74%) create mode 100644 packages/frontend/core/src/blocksuite/manager/common-view.ts create mode 100644 packages/frontend/core/src/blocksuite/manager/editor-view.tsx create mode 100644 packages/frontend/core/src/modules/doc-info/use-std.ts diff --git a/blocksuite/affine/blocks/code/src/view.ts b/blocksuite/affine/blocks/code/src/view.ts index d5395924f7..d5aa50c5b1 100644 --- a/blocksuite/affine/blocks/code/src/view.ts +++ b/blocksuite/affine/blocks/code/src/view.ts @@ -41,7 +41,6 @@ export class CodeBlockViewExtension extends ViewExtensionProvider { FlavourExtension('affine:code'), CodeBlockHighlighter, BlockViewExtension('affine:code', literal`affine-code`), - codeToolbarWidget, SlashMenuConfigExtension('affine:code', codeSlashMenuConfig), CodeKeymapExtension, ...getCodeClipboardExtensions(), @@ -50,5 +49,8 @@ export class CodeBlockViewExtension extends ViewExtensionProvider { CodeBlockInlineManagerExtension, CodeBlockUnitSpecExtension, ]); + if (!this.isMobile(context.scope)) { + context.register(codeToolbarWidget); + } } } diff --git a/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts b/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts index f314b63f71..001c8a2a26 100644 --- a/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts +++ b/blocksuite/affine/ext-loader/src/__tests__/ext-loader.unit.spec.ts @@ -208,21 +208,21 @@ it('should effect only run once', () => { const manager = new ViewExtensionManager([ViewExt1]); - expect(ViewExt1.effectRunned).toBe(false); - expect(ViewExt2.effectRunned).toBe(false); + expect(ViewExt1.effectRan).toBe(false); + expect(ViewExt2.effectRan).toBe(false); manager.get('page'); - expect(ViewExt1.effectRunned).toBe(true); - expect(ViewExt2.effectRunned).toBe(false); + expect(ViewExt1.effectRan).toBe(true); + expect(ViewExt2.effectRan).toBe(false); expect(effect1).toHaveBeenCalledTimes(1); expect(effect2).toHaveBeenCalledTimes(0); manager.get('edgeless'); - expect(ViewExt1.effectRunned).toBe(true); - expect(ViewExt2.effectRunned).toBe(false); + expect(ViewExt1.effectRan).toBe(true); + expect(ViewExt2.effectRan).toBe(false); expect(effect1).toHaveBeenCalledTimes(1); expect(effect2).toHaveBeenCalledTimes(0); diff --git a/blocksuite/affine/ext-loader/src/view-provider.ts b/blocksuite/affine/ext-loader/src/view-provider.ts index 2a6dd4d932..8abb48c8df 100644 --- a/blocksuite/affine/ext-loader/src/view-provider.ts +++ b/blocksuite/affine/ext-loader/src/view-provider.ts @@ -64,7 +64,7 @@ export class ViewExtensionProvider< * Static flag to ensure effect is only run once per provider class * @internal */ - static effectRunned = false; + static effectRan = false; /** * Override this method to implement one-time initialization logic for the provider. @@ -94,11 +94,29 @@ export class ViewExtensionProvider< ); }; + /** + * Check if the scope is preview + * @param scope - The scope to check + * @returns True if the scope is preview, false otherwise + */ + isPreview = (scope: ViewScope) => { + return scope === 'preview-page' || scope === 'preview-edgeless'; + }; + + /** + * Check if the scope is mobile + * @param scope - The scope to check + * @returns True if the scope is mobile, false otherwise + */ + isMobile = (scope: ViewScope) => { + return scope === 'mobile-page' || scope === 'mobile-edgeless'; + }; + override setup(context: ViewExtensionContext, options?: Options) { super.setup(context, options); - const constructer = this.constructor as typeof ViewExtensionProvider; - if (!constructer.effectRunned) { - constructer.effectRunned = true; + const constructor = this.constructor as typeof ViewExtensionProvider; + if (!constructor.effectRan) { + constructor.effectRan = true; this.effect(); } } diff --git a/blocksuite/affine/foundation/src/clipboard.ts b/blocksuite/affine/foundation/src/clipboard.ts new file mode 100644 index 0000000000..57d95ddaf5 --- /dev/null +++ b/blocksuite/affine/foundation/src/clipboard.ts @@ -0,0 +1,65 @@ +import { + AttachmentAdapter, + ClipboardAdapter, + HtmlAdapter, + ImageAdapter, + MixTextAdapter, + NotionTextAdapter, +} from '@blocksuite/affine-shared/adapters'; +import { ClipboardAdapterConfigExtension } from '@blocksuite/std'; +import type { ExtensionType } from '@blocksuite/store'; + +const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({ + mimeType: ClipboardAdapter.MIME, + adapter: ClipboardAdapter, + priority: 100, +}); + +const NotionClipboardConfig = ClipboardAdapterConfigExtension({ + mimeType: 'text/_notion-text-production', + adapter: NotionTextAdapter, + priority: 95, +}); + +const HtmlClipboardConfig = ClipboardAdapterConfigExtension({ + mimeType: 'text/html', + adapter: HtmlAdapter, + priority: 90, +}); + +const imageClipboardConfigs = [ + 'image/apng', + 'image/avif', + 'image/gif', + 'image/jpeg', + 'image/png', + 'image/svg+xml', + 'image/webp', +].map(mimeType => { + return ClipboardAdapterConfigExtension({ + mimeType, + adapter: ImageAdapter, + priority: 80, + }); +}); + +const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({ + mimeType: 'text/plain', + adapter: MixTextAdapter, + priority: 70, +}); + +const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({ + mimeType: '*/*', + adapter: AttachmentAdapter, + priority: 60, +}); + +export const clipboardConfigs: ExtensionType[] = [ + SnapshotClipboardConfig, + NotionClipboardConfig, + HtmlClipboardConfig, + ...imageClipboardConfigs, + PlainTextClipboardConfig, + AttachmentClipboardConfig, +]; diff --git a/blocksuite/affine/foundation/src/view.ts b/blocksuite/affine/foundation/src/view.ts index 517257b415..55ff5bf477 100644 --- a/blocksuite/affine/foundation/src/view.ts +++ b/blocksuite/affine/foundation/src/view.ts @@ -3,14 +3,6 @@ import { type ViewExtensionContext, ViewExtensionProvider, } from '@blocksuite/affine-ext-loader'; -import { - AttachmentAdapter, - ClipboardAdapter, - HtmlAdapter, - ImageAdapter, - MixTextAdapter, - NotionTextAdapter, -} from '@blocksuite/affine-shared/adapters'; import { AutoClearSelectionService, DefaultOpenDocExtension, @@ -25,67 +17,11 @@ import { ThemeService, ToolbarRegistryExtension, } from '@blocksuite/affine-shared/services'; -import { ClipboardAdapterConfigExtension } from '@blocksuite/std'; import { InteractivityManager, ToolController } from '@blocksuite/std/gfx'; -import type { ExtensionType } from '@blocksuite/store'; +import { clipboardConfigs } from './clipboard'; import { effects } from './effects'; -const SnapshotClipboardConfig = ClipboardAdapterConfigExtension({ - mimeType: ClipboardAdapter.MIME, - adapter: ClipboardAdapter, - priority: 100, -}); - -const NotionClipboardConfig = ClipboardAdapterConfigExtension({ - mimeType: 'text/_notion-text-production', - adapter: NotionTextAdapter, - priority: 95, -}); - -const HtmlClipboardConfig = ClipboardAdapterConfigExtension({ - mimeType: 'text/html', - adapter: HtmlAdapter, - priority: 90, -}); - -const imageClipboardConfigs = [ - 'image/apng', - 'image/avif', - 'image/gif', - 'image/jpeg', - 'image/png', - 'image/svg+xml', - 'image/webp', -].map(mimeType => { - return ClipboardAdapterConfigExtension({ - mimeType, - adapter: ImageAdapter, - priority: 80, - }); -}); - -const PlainTextClipboardConfig = ClipboardAdapterConfigExtension({ - mimeType: 'text/plain', - adapter: MixTextAdapter, - priority: 70, -}); - -const AttachmentClipboardConfig = ClipboardAdapterConfigExtension({ - mimeType: '*/*', - adapter: AttachmentAdapter, - priority: 60, -}); - -export const clipboardConfigs: ExtensionType[] = [ - SnapshotClipboardConfig, - NotionClipboardConfig, - HtmlClipboardConfig, - ...imageClipboardConfigs, - PlainTextClipboardConfig, - AttachmentClipboardConfig, -]; - export class FoundationViewExtension extends ViewExtensionProvider { override name = 'foundation'; diff --git a/blocksuite/affine/shared/src/services/theme-service.ts b/blocksuite/affine/shared/src/services/theme-service.ts index 5860e61d10..bf56feebc3 100644 --- a/blocksuite/affine/shared/src/services/theme-service.ts +++ b/blocksuite/affine/shared/src/services/theme-service.ts @@ -6,7 +6,7 @@ import { } from '@blocksuite/affine-model'; import { type Container, createIdentifier } from '@blocksuite/global/di'; import { type BlockStdScope, StdIdentifier } from '@blocksuite/std'; -import { Extension, type ExtensionType } from '@blocksuite/store'; +import { Extension } from '@blocksuite/store'; import { type Signal, signal } from '@preact/signals-core'; import { type AffineCssVariables, @@ -25,14 +25,6 @@ export interface ThemeExtension { getEdgelessTheme?: (docId?: string) => Signal; } -export function OverrideThemeExtension(service: ThemeExtension): ExtensionType { - return { - setup: di => { - di.override(ThemeExtensionIdentifier, () => service); - }, - }; -} - export const ThemeProvider = createIdentifier( 'AffineThemeProvider' ); @@ -78,6 +70,7 @@ export class ThemeService extends Extension { * * @param color - A color value. * @param fallback - If color value processing fails, it will be used as a fallback. + * @param theme - Target theme, default is the current theme. * @returns - A color property string. * * @example @@ -112,6 +105,7 @@ export class ThemeService extends Extension { * @param color - A color value. * @param fallback - If color value processing fails, it will be used as a fallback. * @param real - If true, it returns the computed style. + * @param theme - Target theme, default is the current theme. * @returns - A color property string. * * @example diff --git a/blocksuite/affine/widgets/slash-menu/src/view.ts b/blocksuite/affine/widgets/slash-menu/src/view.ts index ff717e49b6..18fbdba1c4 100644 --- a/blocksuite/affine/widgets/slash-menu/src/view.ts +++ b/blocksuite/affine/widgets/slash-menu/src/view.ts @@ -16,6 +16,7 @@ export class SlashMenuViewExtension extends ViewExtensionProvider { override setup(context: ViewExtensionContext) { super.setup(context); + if (this.isMobile(context.scope)) return; context.register(SlashMenuExtension); } } diff --git a/blocksuite/affine/widgets/toolbar/src/view.ts b/blocksuite/affine/widgets/toolbar/src/view.ts index 21ae72b400..87673b8334 100644 --- a/blocksuite/affine/widgets/toolbar/src/view.ts +++ b/blocksuite/affine/widgets/toolbar/src/view.ts @@ -16,6 +16,7 @@ export class ToolbarViewExtension extends ViewExtensionProvider { override setup(context: ViewExtensionContext) { super.setup(context); + if (this.isMobile(context.scope)) return; context.register(toolbarWidget); } } diff --git a/packages/frontend/component/src/lit-react/lit-portal/index.ts b/packages/frontend/component/src/lit-react/lit-portal/index.ts index c7005b2075..f6d3dbe3f4 100644 --- a/packages/frontend/component/src/lit-react/lit-portal/index.ts +++ b/packages/frontend/component/src/lit-react/lit-portal/index.ts @@ -1,5 +1,6 @@ export { type ElementOrFactory, + type ReactToLit, useLitPortal, useLitPortalFactory, } from './lit-portal'; diff --git a/packages/frontend/component/src/lit-react/lit-portal/lit-portal.tsx b/packages/frontend/component/src/lit-react/lit-portal/lit-portal.tsx index 21b161ecaf..fd525a080f 100644 --- a/packages/frontend/component/src/lit-react/lit-portal/lit-portal.tsx +++ b/packages/frontend/component/src/lit-react/lit-portal/lit-portal.tsx @@ -1,4 +1,4 @@ -import { html, LitElement } from 'lit'; +import { html, LitElement, type TemplateResult } from 'lit'; import { customElement } from 'lit/decorators.js'; import { nanoid } from 'nanoid'; import { useCallback, useMemo, useState } from 'react'; @@ -82,59 +82,60 @@ type LitPortal = { litElement: LitReactPortal; }; +export type ReactToLit = ( + elementOrFactory: ElementOrFactory, + rerendering?: boolean +) => TemplateResult; + // returns a factory function that renders a given element to a lit template export const useLitPortalFactory = () => { const [portals, setPortals] = useState([]); - return [ - useCallback( - ( - elementOrFactory: React.ReactElement | (() => React.ReactElement), - rerendering = true - ) => { - const element = - typeof elementOrFactory === 'function' - ? elementOrFactory() - : elementOrFactory; - return createLitPortalAnchor(event => { - setPortals(portals => { - const { name, target } = event; - const id = target.portalId; - let newPortals = portals; - const updatePortals = () => { - let oldPortalIndex = portals.findIndex( - p => p.litElement === target - ); - oldPortalIndex = - oldPortalIndex === -1 ? portals.length : oldPortalIndex; - newPortals = portals.toSpliced(oldPortalIndex, 1, { - id, - portal: ReactDOM.createPortal(element, target), - litElement: target, - }); - }; - switch (name) { - case 'connectedCallback': - updatePortals(); + const reactToLit: ReactToLit = useCallback( + (elementOrFactory, rerendering) => { + const element = + typeof elementOrFactory === 'function' + ? elementOrFactory() + : elementOrFactory; + return createLitPortalAnchor(event => { + setPortals(portals => { + const { name, target } = event; + const id = target.portalId; + let newPortals = portals; + const updatePortals = () => { + let oldPortalIndex = portals.findIndex( + p => p.litElement === target + ); + oldPortalIndex = + oldPortalIndex === -1 ? portals.length : oldPortalIndex; + newPortals = portals.toSpliced(oldPortalIndex, 1, { + id, + portal: ReactDOM.createPortal(element, target), + litElement: target, + }); + }; + switch (name) { + case 'connectedCallback': + updatePortals(); + break; + case 'disconnectedCallback': + newPortals = portals.filter(p => p.litElement.isConnected); + break; + case 'willUpdate': + if (!target.isConnected || !rerendering) { break; - case 'disconnectedCallback': - newPortals = portals.filter(p => p.litElement.isConnected); - break; - case 'willUpdate': - if (!target.isConnected || !rerendering) { - break; - } - updatePortals(); - break; - } - return newPortals; - }); + } + updatePortals(); + break; + } + return newPortals; }); - }, - [setPortals] - ), - portals, - ] as const; + }); + }, + [] + ); + + return [reactToLit, portals] as const; }; // render a react element to a lit template 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 df33b78d3b..033e7badf8 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 @@ -6,22 +6,17 @@ import { LitEdgelessEditor, type PageEditor, } from '@affine/core/blocksuite/editors'; +import type { AffineEditorViewOptions } from '@affine/core/blocksuite/manager/editor-view'; import { useEnableAI } from '@affine/core/components/hooks/affine/use-enable-ai'; -import { PublicUserService } from '@affine/core/modules/cloud'; import type { DocCustomPropertyInfo } from '@affine/core/modules/db'; -import { DocService, DocsService } from '@affine/core/modules/doc'; import type { DatabaseRow, DatabaseValueCell, } from '@affine/core/modules/doc-info/types'; -import { EditorService } from '@affine/core/modules/editor'; import { EditorSettingService } from '@affine/core/modules/editor-setting'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { JournalService } from '@affine/core/modules/journal'; -import { toDocSearchParams } from '@affine/core/modules/navigation'; import { useInsidePeekView } from '@affine/core/modules/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 track from '@affine/track'; import type { DocTitle } from '@blocksuite/affine/fragments/doc-title'; @@ -43,41 +38,11 @@ import { useRef, } from 'react'; -import { - AffinePageReference, - AffineSharedPageReference, -} from '../../components/affine/reference-link'; import { type DefaultOpenProperty, DocPropertiesTable, } from '../../components/doc-properties'; -import { - patchForAudioEmbedView, - patchForPDFEmbedView, -} from '../extensions/attachment-embed-view'; -import { patchDatabaseBlockConfigService } from '../extensions/database-block-config-service'; -import { patchDocModeService } from '../extensions/doc-mode-service'; -import { patchDocUrlExtensions } from '../extensions/doc-url'; -import { - patchForEdgelessNoteConfig, - patchForEmbedSyncedDocConfig, -} from '../extensions/edgeless-block-header'; -import { EdgelessClipboardAIChatConfig } from '../extensions/edgeless-clipboard'; -import { patchForClipboardInElectron } from '../extensions/electron-clipboard'; import { enableEditorExtension } from '../extensions/entry/enable-editor'; -import { enableMobileExtension } from '../extensions/entry/enable-mobile'; -import { patchNotificationService } from '../extensions/notification-service'; -import { patchOpenDocExtension } from '../extensions/open-doc'; -import { patchPeekViewService } from '../extensions/peek-view-service'; -import { patchQuickSearchService } from '../extensions/quick-search-service'; -import { - patchReferenceRenderer, - type ReferenceReactRenderer, -} from '../extensions/reference-renderer'; -import { patchSideBarService } from '../extensions/side-bar-service'; -import { patchTurboRendererExtension } from '../extensions/turbo-renderer'; -import { patchUserExtensions } from '../extensions/user'; -import { patchUserListExtensions } from '../extensions/user-list'; import { BiDirectionalLinkPanel } from './bi-directional-link-panel'; import { BlocksuiteEditorJournalDocTitle } from './journal-doc-title'; import { StarterBar } from './starter-bar'; @@ -92,55 +57,12 @@ interface BlocksuiteEditorProps { const usePatchSpecs = (mode: DocMode) => { const [reactToLit, portals] = useLitPortalFactory(); - const { - peekViewService, - docService, - docsService, - editorService, - workspaceService, - featureFlagService, - memberSearchService, - publicUserService, - } = useServices({ - PeekViewService, - DocService, - DocsService, + const { workspaceService, featureFlagService } = useServices({ WorkspaceService, - EditorService, FeatureFlagService, - MemberSearchService, - PublicUserService, }); const isCloud = workspaceService.workspace.flavour !== 'local'; const framework = useFramework(); - const referenceRenderer: ReferenceReactRenderer = useMemo(() => { - return function customReference(reference) { - const data = reference.delta.attributes?.reference; - if (!data) return ; - - const pageId = data.pageId; - if (!pageId) return ; - - // title alias - const title = data.title; - const params = toDocSearchParams(data.params); - - if (workspaceService.workspace.openOptions.isSharedMode) { - return ( - - ); - } - - return ( - - ); - }; - }, [workspaceService]); const confirmModal = useConfirmModal(); @@ -158,70 +80,33 @@ const usePatchSpecs = (mode: DocMode) => { ) ); - const patchedSpecs = useMemo(() => { - let extensions = enableEditorExtension(framework, mode, enableAI); + const editorOptions: AffineEditorViewOptions = useMemo(() => { + return { + isCloud, + isInPeekView: insidePeekView, - extensions = extensions.concat( - [ - patchReferenceRenderer(reactToLit, referenceRenderer), - patchForEdgelessNoteConfig(framework, reactToLit, insidePeekView), - patchForEmbedSyncedDocConfig(reactToLit), - patchNotificationService(confirmModal), - patchPeekViewService(peekViewService), - patchOpenDocExtension(), - EdgelessClipboardAIChatConfig, - patchDocUrlExtensions(framework), - patchQuickSearchService(framework), - patchSideBarService(framework), - patchDocModeService(docService, docsService, editorService), - patchForAudioEmbedView(reactToLit), - isCloud - ? [ - patchUserListExtensions(memberSearchService), - patchUserExtensions(publicUserService), - ] - : [], - patchDatabaseBlockConfigService(), - mode === 'edgeless' && enableTurboRenderer - ? patchTurboRendererExtension() - : [], - ].flat() - ); + enableTurboRenderer, + enablePDFEmbedPreview, - if (enablePDFEmbedPreview) { - extensions = extensions.concat([patchForPDFEmbedView(reactToLit)]); - } + framework, - if (BUILD_CONFIG.isMobileEdition) { - extensions = enableMobileExtension(extensions, framework); - } - - if (BUILD_CONFIG.isElectron) { - extensions = extensions.concat( - [patchForClipboardInElectron(framework)].flat() - ); - } - - return extensions; + reactToLit: reactToLit as AffineEditorViewOptions['reactToLit'], + confirmModal, + }; }, [ - framework, - mode, - enableAI, - reactToLit, - referenceRenderer, - insidePeekView, confirmModal, - peekViewService, - docService, - docsService, - editorService, - isCloud, - memberSearchService, - publicUserService, - enableTurboRenderer, enablePDFEmbedPreview, + enableTurboRenderer, + framework, + insidePeekView, + isCloud, + reactToLit, ]); + const patchedSpecs = useMemo(() => { + return enableEditorExtension(framework, mode, enableAI, editorOptions); + }, [framework, mode, enableAI, editorOptions]); + return [ patchedSpecs, useMemo( diff --git a/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx b/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx index ec33cd3b39..37d8a11747 100644 --- a/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx +++ b/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx @@ -1,16 +1,10 @@ -import type { ElementOrFactory } from '@affine/component'; +import type { ReactToLit } from '@affine/component'; import { AttachmentEmbedPreview } from '@affine/core/blocksuite/attachment-viewer/attachment-embed-preview'; import { AttachmentEmbedConfigIdentifier } from '@blocksuite/affine/blocks/attachment'; import { Bound } from '@blocksuite/affine/global/gfx'; import type { ExtensionType } from '@blocksuite/affine/store'; -import type { TemplateResult } from 'lit'; -export function patchForPDFEmbedView( - reactToLit: ( - element: ElementOrFactory, - rerendering?: boolean - ) => TemplateResult -): ExtensionType { +export function patchForPDFEmbedView(reactToLit: ReactToLit): ExtensionType { return { setup: di => { di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({ @@ -35,12 +29,7 @@ export function patchForPDFEmbedView( }; } -export function patchForAudioEmbedView( - reactToLit: ( - element: ElementOrFactory, - rerendering?: boolean - ) => TemplateResult -): ExtensionType { +export function patchForAudioEmbedView(reactToLit: ReactToLit): ExtensionType { return { setup: di => { di.override(AttachmentEmbedConfigIdentifier('audio'), () => ({ diff --git a/packages/frontend/core/src/blocksuite/extensions/edgeless-block-header/index.tsx b/packages/frontend/core/src/blocksuite/extensions/edgeless-block-header/index.tsx index 06ad54cd8f..b820cca012 100644 --- a/packages/frontend/core/src/blocksuite/extensions/edgeless-block-header/index.tsx +++ b/packages/frontend/core/src/blocksuite/extensions/edgeless-block-header/index.tsx @@ -1,4 +1,4 @@ -import type { ElementOrFactory } from '@affine/component'; +import type { ReactToLit } from '@affine/component'; import { JournalService } from '@affine/core/modules/journal'; import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc'; import { NoteConfigExtension } from '@blocksuite/affine/blocks/note'; @@ -10,7 +10,7 @@ import { } from '@blocksuite/affine/shared/services'; import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx'; import type { FrameworkProvider } from '@toeverything/infra'; -import { html, type TemplateResult } from 'lit'; +import { html } from 'lit'; import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title'; import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header'; @@ -18,7 +18,7 @@ import { EdgelessNoteHeader } from './edgeless-note-header'; export function patchForEdgelessNoteConfig( framework: FrameworkProvider, - reactToLit: (element: ElementOrFactory) => TemplateResult, + reactToLit: ReactToLit, insidePeekView: boolean ) { return NoteConfigExtension({ @@ -100,9 +100,7 @@ export function patchForEdgelessNoteConfig( }); } -export function patchForEmbedSyncedDocConfig( - reactToLit: (element: ElementOrFactory) => TemplateResult -) { +export function patchForEmbedSyncedDocConfig(reactToLit: ReactToLit) { return EmbedSyncedDocConfigExtension({ edgelessHeader: ({ model, std }) => reactToLit(), diff --git a/packages/frontend/core/src/blocksuite/extensions/entry/enable-editor.ts b/packages/frontend/core/src/blocksuite/extensions/entry/enable-editor.ts index ae92876687..08ee7365f6 100644 --- a/packages/frontend/core/src/blocksuite/extensions/entry/enable-editor.ts +++ b/packages/frontend/core/src/blocksuite/extensions/entry/enable-editor.ts @@ -1,3 +1,4 @@ +import type { AffineEditorViewOptions } from '@affine/core/blocksuite/manager/editor-view'; import type { ExtensionType } from '@blocksuite/affine/store'; import { type FrameworkProvider } from '@toeverything/infra'; @@ -6,8 +7,16 @@ import { getViewManager } from '../../manager/migrating-view'; export function enableEditorExtension( framework: FrameworkProvider, mode: 'edgeless' | 'page', - enableAI: boolean + enableAI: boolean, + options: AffineEditorViewOptions ): ExtensionType[] { - const manager = getViewManager(framework, enableAI); + const manager = getViewManager(framework, enableAI, options); + if (BUILD_CONFIG.isMobileEdition) { + if (mode === 'page') { + return manager.get('mobile-page'); + } + + return manager.get('mobile-edgeless'); + } return manager.get(mode); } diff --git a/packages/frontend/core/src/blocksuite/extensions/entry/enable-preview.ts b/packages/frontend/core/src/blocksuite/extensions/entry/enable-preview.ts deleted file mode 100644 index 236aae424e..0000000000 --- a/packages/frontend/core/src/blocksuite/extensions/entry/enable-preview.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AppThemeService } from '@affine/core/modules/theme'; -import type { Container } from '@blocksuite/affine/global/di'; -import { ColorScheme } from '@blocksuite/affine/model'; -import { - type ThemeExtension, - ThemeExtensionIdentifier, -} from '@blocksuite/affine/shared/services'; -import { - createSignalFromObservable, - type Signal, -} from '@blocksuite/affine/shared/utils'; -import { - type BlockStdScope, - LifeCycleWatcher, - StdIdentifier, -} from '@blocksuite/affine/std'; -import type { FrameworkProvider } from '@toeverything/infra'; -import type { Observable } from 'rxjs'; - -export function getPagePreviewThemeExtension(framework: FrameworkProvider) { - class AffinePagePreviewThemeExtension - extends LifeCycleWatcher - implements ThemeExtension - { - static override readonly key = 'affine-page-preview-theme'; - - readonly theme: Signal; - - readonly disposables: (() => void)[] = []; - - static override setup(di: Container) { - super.setup(di); - di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [ - StdIdentifier, - ]); - } - - constructor(std: BlockStdScope) { - super(std); - const theme$: Observable = framework - .get(AppThemeService) - .appTheme.theme$.map(theme => { - return theme === ColorScheme.Dark - ? ColorScheme.Dark - : ColorScheme.Light; - }); - const { signal, cleanup } = createSignalFromObservable( - theme$, - ColorScheme.Light - ); - this.theme = signal; - this.disposables.push(cleanup); - } - - getAppTheme() { - return this.theme; - } - - getEdgelessTheme() { - return this.theme; - } - - override unmounted() { - this.dispose(); - } - - dispose() { - this.disposables.forEach(dispose => dispose()); - } - } - - return AffinePagePreviewThemeExtension; -} diff --git a/packages/frontend/core/src/blocksuite/extensions/index.ts b/packages/frontend/core/src/blocksuite/extensions/index.ts deleted file mode 100644 index 025a1597dc..0000000000 --- a/packages/frontend/core/src/blocksuite/extensions/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './entry/enable-mobile'; diff --git a/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts b/packages/frontend/core/src/blocksuite/extensions/mobile-config.ts similarity index 74% rename from packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts rename to packages/frontend/core/src/blocksuite/extensions/mobile-config.ts index 9f739d9dc0..2e85f890b9 100644 --- a/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts +++ b/packages/frontend/core/src/blocksuite/extensions/mobile-config.ts @@ -1,8 +1,5 @@ import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard'; -import { - CodeBlockConfigExtension, - codeToolbarWidget, -} from '@blocksuite/affine/blocks/code'; +import { CodeBlockConfigExtension } from '@blocksuite/affine/blocks/code'; import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph'; import type { Container } from '@blocksuite/affine/global/di'; import { DisposableGroup } from '@blocksuite/affine/global/disposable'; @@ -13,12 +10,10 @@ import { } from '@blocksuite/affine/shared/services'; import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std'; import type { ExtensionType } from '@blocksuite/affine/store'; -import { SlashMenuExtension } from '@blocksuite/affine/widgets/slash-menu'; -import { toolbarWidget } from '@blocksuite/affine/widgets/toolbar'; import { batch, signal } from '@preact/signals-core'; import type { FrameworkProvider } from '@toeverything/infra'; -class MobileSpecsPatches extends LifeCycleWatcher { +export class MobileSpecsPatches extends LifeCycleWatcher { static override key = 'mobile-patches'; constructor(std: BlockStdScope) { @@ -30,7 +25,7 @@ class MobileSpecsPatches extends LifeCycleWatcher { } } -const mobileParagraphConfig = ParagraphBlockConfigExtension({ +export const mobileParagraphConfig = ParagraphBlockConfigExtension({ getPlaceholder: model => { const placeholders = { text: '', @@ -46,11 +41,13 @@ const mobileParagraphConfig = ParagraphBlockConfigExtension({ }, }); -const mobileCodeConfig = CodeBlockConfigExtension({ +export const mobileCodeConfig = CodeBlockConfigExtension({ showLineNumbers: false, }); -function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType { +export function KeyboardToolbarExtension( + framework: FrameworkProvider +): ExtensionType { const affineVirtualKeyboardProvider = framework.get(VirtualKeyboardProvider); class BSVirtualKeyboardService @@ -109,28 +106,3 @@ function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType { return BSVirtualKeyboardService; } - -export function enableMobileExtension( - extensions: ExtensionType[], - framework: FrameworkProvider -): ExtensionType[] { - const next = extensions.filter(extension => { - if (extension === codeToolbarWidget) { - return false; - } - if (extension === toolbarWidget) { - return false; - } - if (extension === SlashMenuExtension) { - return false; - } - return true; - }); - next.push( - MobileSpecsPatches, - KeyboardToolbarExtension(framework), - mobileParagraphConfig, - mobileCodeConfig - ); - return next; -} diff --git a/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts b/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts index 19a00e0424..939ddfb3b0 100644 --- a/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts +++ b/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts @@ -1,20 +1,19 @@ -import type { ElementOrFactory } from '@affine/component'; +import type { ReactToLit } from '@affine/component'; import type { AffineReference } from '@blocksuite/affine/inlines/reference'; import { ReferenceNodeConfigExtension } from '@blocksuite/affine/inlines/reference'; import type { ExtensionType } from '@blocksuite/affine/store'; -import type { TemplateResult } from 'lit'; export type ReferenceReactRenderer = ( reference: AffineReference ) => React.ReactElement; export function patchReferenceRenderer( - reactToLit: (element: ElementOrFactory) => TemplateResult, + reactToLit: ReactToLit, reactRenderer: ReferenceReactRenderer ): ExtensionType { const customContent = (reference: AffineReference) => { const node = reactRenderer(reference); - return reactToLit(node); + return reactToLit(node, true); }; return ReferenceNodeConfigExtension({ diff --git a/packages/frontend/core/src/blocksuite/extensions/theme.ts b/packages/frontend/core/src/blocksuite/extensions/theme.ts index 2ee25ae72b..950d0bce4b 100644 --- a/packages/frontend/core/src/blocksuite/extensions/theme.ts +++ b/packages/frontend/core/src/blocksuite/extensions/theme.ts @@ -10,7 +10,11 @@ import { createSignalFromObservable, type Signal, } from '@blocksuite/affine/shared/utils'; -import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/std'; +import { + type BlockStdScope, + LifeCycleWatcher, + StdIdentifier, +} from '@blocksuite/affine/std'; import { type FrameworkProvider } from '@toeverything/infra'; import type { Observable } from 'rxjs'; import { combineLatest, map } from 'rxjs'; @@ -95,3 +99,58 @@ export function getThemeExtension( return AffineThemeExtension; } + +export function getPreviewThemeExtension(framework: FrameworkProvider) { + class AffinePagePreviewThemeExtension + extends LifeCycleWatcher + implements ThemeExtension + { + static override readonly key = 'affine-page-preview-theme'; + + readonly theme: Signal; + + readonly disposables: (() => void)[] = []; + + static override setup(di: Container) { + super.setup(di); + di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [ + StdIdentifier, + ]); + } + + constructor(std: BlockStdScope) { + super(std); + const theme$: Observable = framework + .get(AppThemeService) + .appTheme.theme$.map(theme => { + return theme === ColorScheme.Dark + ? ColorScheme.Dark + : ColorScheme.Light; + }); + const { signal, cleanup } = createSignalFromObservable( + theme$, + ColorScheme.Light + ); + this.theme = signal; + this.disposables.push(cleanup); + } + + getAppTheme() { + return this.theme; + } + + getEdgelessTheme() { + return this.theme; + } + + override unmounted() { + this.dispose(); + } + + dispose() { + this.disposables.forEach(dispose => dispose()); + } + } + + return AffinePagePreviewThemeExtension; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts b/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts index 533d791cea..dc90be6559 100644 --- a/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts +++ b/packages/frontend/core/src/blocksuite/extensions/turbo-renderer.ts @@ -14,20 +14,18 @@ function createPainterWorker() { return worker; } -export function patchTurboRendererExtension() { - return [ - ParagraphLayoutHandlerExtension, - ListLayoutHandlerExtension, - NoteLayoutHandlerExtension, - CodeLayoutHandlerExtension, - ImageLayoutHandlerExtension, - TurboRendererConfigFactory({ - options: { - zoomThreshold: 1, - debounceTime: 1000, - }, - painterWorkerEntry: createPainterWorker, - }), - ViewportTurboRendererExtension, - ]; -} +export const turboRendererExtension = [ + ParagraphLayoutHandlerExtension, + ListLayoutHandlerExtension, + NoteLayoutHandlerExtension, + CodeLayoutHandlerExtension, + ImageLayoutHandlerExtension, + TurboRendererConfigFactory({ + options: { + zoomThreshold: 1, + debounceTime: 1000, + }, + painterWorkerEntry: createPainterWorker, + }), + ViewportTurboRendererExtension, +]; diff --git a/packages/frontend/core/src/blocksuite/manager/common-view.ts b/packages/frontend/core/src/blocksuite/manager/common-view.ts new file mode 100644 index 0000000000..eadd3cfae1 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/manager/common-view.ts @@ -0,0 +1,141 @@ +import { toolbarAIEntryConfig } from '@affine/core/blocksuite/ai'; +import { AIChatBlockSpec } from '@affine/core/blocksuite/ai/blocks'; +import { AITranscriptionBlockSpec } from '@affine/core/blocksuite/ai/blocks/ai-chat-block/ai-transcription-block'; +import { edgelessToolbarAIEntryConfig } from '@affine/core/blocksuite/ai/entries/edgeless'; +import { imageToolbarAIEntryConfig } from '@affine/core/blocksuite/ai/entries/image-toolbar/setup-image-toolbar'; +import { AICodeBlockWatcher } from '@affine/core/blocksuite/ai/extensions/ai-code'; +import { getAIEdgelessRootWatcher } from '@affine/core/blocksuite/ai/extensions/ai-edgeless-root'; +import { getAIPageRootWatcher } from '@affine/core/blocksuite/ai/extensions/ai-page-root'; +import { AiSlashMenuConfigExtension } from '@affine/core/blocksuite/ai/extensions/ai-slash-menu'; +import { CopilotTool } from '@affine/core/blocksuite/ai/tool/copilot-tool'; +import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel'; +import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot'; +import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta'; +import { getEditorConfigExtension } from '@affine/core/blocksuite/extensions/editor-config'; +import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit'; +import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config'; +import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service'; +import { getTelemetryExtension } from '@affine/core/blocksuite/extensions/telemetry'; +import { + getPreviewThemeExtension, + getThemeExtension, +} from '@affine/core/blocksuite/extensions/theme'; +import { PeekViewService } from '@affine/core/modules/peek-view'; +import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph'; +import { + type ViewExtensionContext, + ViewExtensionProvider, +} from '@blocksuite/affine/ext-loader'; +import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services'; +import { BlockFlavourIdentifier } from '@blocksuite/affine/std'; +import { FrameworkProvider } from '@toeverything/infra'; +import { z } from 'zod'; + +const optionsSchema = z.object({ + enableAI: z.boolean().optional(), + framework: z.instanceof(FrameworkProvider).optional(), +}); + +export class AffineCommonViewExtension extends ViewExtensionProvider< + z.infer +> { + override name = 'affine-view-extensions'; + + override schema = optionsSchema; + + private _setupTheme( + context: ViewExtensionContext, + framework: FrameworkProvider + ) { + if (this.isPreview(context.scope)) { + context.register(getPreviewThemeExtension(framework)); + } else { + context.register(getThemeExtension(framework)); + } + } + + private _setupAI( + context: ViewExtensionContext, + framework: FrameworkProvider + ) { + context.register(AIChatBlockSpec); + context.register(AITranscriptionBlockSpec); + context.register( + [ + AICodeBlockWatcher, + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('custom:affine:image'), + config: imageToolbarAIEntryConfig(), + }), + ParagraphBlockConfigExtension({ + getPlaceholder: model => { + const placeholders = { + text: "Type '/' for commands, 'space' for AI", + h1: 'Heading 1', + h2: 'Heading 2', + h3: 'Heading 3', + h4: 'Heading 4', + h5: 'Heading 5', + h6: 'Heading 6', + quote: '', + }; + return placeholders[model.props.type]; + }, + }), + ].flat() + ); + if (context.scope === 'edgeless') { + context.register([ + CopilotTool, + aiPanelWidget, + edgelessCopilotWidget, + getAIEdgelessRootWatcher(framework), + // In note + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('custom:affine:note'), + config: toolbarAIEntryConfig(), + }), + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('custom:affine:surface:*'), + config: edgelessToolbarAIEntryConfig(), + }), + AiSlashMenuConfigExtension(), + ]); + } + if (context.scope === 'page') { + context.register([ + aiPanelWidget, + getAIPageRootWatcher(framework), + ToolbarModuleExtension({ + id: BlockFlavourIdentifier('custom:affine:note'), + config: toolbarAIEntryConfig(), + }), + AiSlashMenuConfigExtension(), + ]); + } + } + + override setup( + context: ViewExtensionContext, + options?: z.infer + ) { + super.setup(context); + const { framework, enableAI } = options || {}; + if (framework) { + context.register(patchPeekViewService(framework.get(PeekViewService))); + context.register([ + getFontConfigExtension(), + buildDocDisplayMetaExtension(framework), + ]); + context.register(getTelemetryExtension()); + this._setupTheme(context, framework); + if (context.scope === 'edgeless' || context.scope === 'page') { + context.register(getEditorConfigExtension(framework)); + context.register(patchFileSizeLimitExtension(framework)); + } + if (enableAI) { + this._setupAI(context, framework); + } + } + } +} diff --git a/packages/frontend/core/src/blocksuite/manager/editor-view.tsx b/packages/frontend/core/src/blocksuite/manager/editor-view.tsx new file mode 100644 index 0000000000..a6c5737d81 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/manager/editor-view.tsx @@ -0,0 +1,187 @@ +import type { ConfirmModalProps, ElementOrFactory } from '@affine/component'; +import { + patchForAudioEmbedView, + patchForPDFEmbedView, +} from '@affine/core/blocksuite/extensions/attachment-embed-view'; +import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service'; +import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service'; +import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url'; +import { + patchForEdgelessNoteConfig, + patchForEmbedSyncedDocConfig, +} from '@affine/core/blocksuite/extensions/edgeless-block-header'; +import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard'; +import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard'; +import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service'; +import { patchOpenDocExtension } from '@affine/core/blocksuite/extensions/open-doc'; +import { patchQuickSearchService } from '@affine/core/blocksuite/extensions/quick-search-service'; +import { + patchReferenceRenderer, + type ReferenceReactRenderer, +} from '@affine/core/blocksuite/extensions/reference-renderer'; +import { patchSideBarService } from '@affine/core/blocksuite/extensions/side-bar-service'; +import { turboRendererExtension } from '@affine/core/blocksuite/extensions/turbo-renderer'; +import { patchUserExtensions } from '@affine/core/blocksuite/extensions/user'; +import { patchUserListExtensions } from '@affine/core/blocksuite/extensions/user-list'; +import { + AffinePageReference, + AffineSharedPageReference, +} from '@affine/core/components/affine/reference-link'; +import { PublicUserService } from '@affine/core/modules/cloud'; +import { DocService, DocsService } from '@affine/core/modules/doc'; +import { EditorService } from '@affine/core/modules/editor'; +import { toDocSearchParams } from '@affine/core/modules/navigation'; +import { MemberSearchService } from '@affine/core/modules/permissions'; +import { WorkspaceService } from '@affine/core/modules/workspace'; +import { + type ViewExtensionContext, + ViewExtensionProvider, +} from '@blocksuite/affine/ext-loader'; +import { FrameworkProvider } from '@toeverything/infra'; +import type { TemplateResult } from 'lit'; +import { z } from 'zod'; + +import { + KeyboardToolbarExtension, + mobileCodeConfig, + mobileParagraphConfig, + MobileSpecsPatches, +} from '../extensions/mobile-config'; + +const optionsSchema = z.object({ + // env + isCloud: z.boolean(), + isInPeekView: z.boolean(), + + // flags + enableTurboRenderer: z.boolean(), + enablePDFEmbedPreview: z.boolean(), + + // services + framework: z.instanceof(FrameworkProvider), + + // react renderer + reactToLit: z + .function() + .args(z.custom(), z.boolean().optional()) + .returns(z.custom()), + confirmModal: z.object({ + openConfirmModal: z + .function() + .args(z.custom().optional(), z.any().optional()), + closeConfirmModal: z.function(), + }), +}); + +export type AffineEditorViewOptions = z.infer; + +export class AffineEditorViewExtension extends ViewExtensionProvider { + override name = 'affine-editor-view'; + + override schema = optionsSchema; + + private readonly _getCustomReferenceRenderer = ( + framework: FrameworkProvider + ): ReferenceReactRenderer => { + const workspaceService = framework.get(WorkspaceService); + return function customReference(reference) { + const data = reference.delta.attributes?.reference; + if (!data) return ; + + const pageId = data.pageId; + if (!pageId) return ; + + // title alias + const title = data.title; + const params = toDocSearchParams(data.params); + + if (workspaceService.workspace.openOptions.isSharedMode) { + return ( + + ); + } + + return ( + + ); + }; + }; + + override setup( + context: ViewExtensionContext, + options?: AffineEditorViewOptions + ) { + super.setup(context); + if (!options) { + return; + } + const { + isCloud, + isInPeekView, + + enableTurboRenderer, + enablePDFEmbedPreview, + + framework, + + reactToLit, + confirmModal, + } = options; + const isEdgeless = this.isEdgeless(context.scope); + const isMobileEdition = BUILD_CONFIG.isMobileEdition; + const isElectron = BUILD_CONFIG.isElectron; + + const docService = framework.get(DocService); + const docsService = framework.get(DocsService); + const editorService = framework.get(EditorService); + const memberSearchService = framework.get(MemberSearchService); + const publicUserService = framework.get(PublicUserService); + + const referenceRenderer = this._getCustomReferenceRenderer(framework); + + context.register([ + patchReferenceRenderer(reactToLit, referenceRenderer), + patchNotificationService(confirmModal), + patchOpenDocExtension(), + EdgelessClipboardAIChatConfig, + patchSideBarService(framework), + patchDocModeService(docService, docsService, editorService), + ]); + context.register(patchDocUrlExtensions(framework)); + context.register(patchQuickSearchService(framework)); + context.register([ + patchForEmbedSyncedDocConfig(reactToLit), + patchForEdgelessNoteConfig(framework, reactToLit, isInPeekView), + patchDatabaseBlockConfigService(), + patchForAudioEmbedView(reactToLit), + ]); + if (isCloud) { + context.register([ + patchUserListExtensions(memberSearchService), + patchUserExtensions(publicUserService), + ]); + } + if (isEdgeless && enableTurboRenderer) { + context.register(turboRendererExtension); + } + if (enablePDFEmbedPreview) { + context.register(patchForPDFEmbedView(reactToLit)); + } + if (isMobileEdition) { + context.register([ + KeyboardToolbarExtension(framework), + MobileSpecsPatches, + mobileParagraphConfig, + mobileCodeConfig, + ]); + } + if (isElectron) { + context.register(patchForClipboardInElectron(framework)); + } + } +} diff --git a/packages/frontend/core/src/blocksuite/manager/migrating-view.ts b/packages/frontend/core/src/blocksuite/manager/migrating-view.ts index fee547b8eb..3d456277d8 100644 --- a/packages/frontend/core/src/blocksuite/manager/migrating-view.ts +++ b/packages/frontend/core/src/blocksuite/manager/migrating-view.ts @@ -1,147 +1,28 @@ -import { PeekViewService } from '@affine/core/modules/peek-view'; -import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph'; +import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view'; import { - type ViewExtensionContext, - ViewExtensionManager, - ViewExtensionProvider, -} from '@blocksuite/affine/ext-loader'; + AffineEditorViewExtension, + type AffineEditorViewOptions, +} from '@affine/core/blocksuite/manager/editor-view'; +import { ViewExtensionManager } from '@blocksuite/affine/ext-loader'; import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view'; -import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services'; -import { BlockFlavourIdentifier } from '@blocksuite/affine/std'; -import { FrameworkProvider } from '@toeverything/infra'; -import { z } from 'zod'; - -import { toolbarAIEntryConfig } from '../ai'; -import { AIChatBlockSpec } from '../ai/blocks'; -import { AITranscriptionBlockSpec } from '../ai/blocks/ai-chat-block/ai-transcription-block'; -import { edgelessToolbarAIEntryConfig } from '../ai/entries/edgeless'; -import { imageToolbarAIEntryConfig } from '../ai/entries/image-toolbar/setup-image-toolbar'; -import { AICodeBlockWatcher } from '../ai/extensions/ai-code'; -import { getAIEdgelessRootWatcher } from '../ai/extensions/ai-edgeless-root'; -import { getAIPageRootWatcher } from '../ai/extensions/ai-page-root'; -import { AiSlashMenuConfigExtension } from '../ai/extensions/ai-slash-menu'; -import { CopilotTool } from '../ai/tool/copilot-tool'; -import { aiPanelWidget } from '../ai/widgets/ai-panel/ai-panel'; -import { edgelessCopilotWidget } from '../ai/widgets/edgeless-copilot'; -import { buildDocDisplayMetaExtension } from '../extensions/display-meta'; -import { getEditorConfigExtension } from '../extensions/editor-config'; -import { getPagePreviewThemeExtension } from '../extensions/entry/enable-preview'; -import { patchFileSizeLimitExtension } from '../extensions/file-size-limit'; -import { getFontConfigExtension } from '../extensions/font-config'; -import { patchPeekViewService } from '../extensions/peek-view-service'; -import { getTelemetryExtension } from '../extensions/telemetry'; -import { getThemeExtension } from '../extensions/theme'; - -const optionsSchema = z.object({ - enableAI: z.boolean().optional(), - framework: z.instanceof(FrameworkProvider).optional(), -}); - -class MigratingAffineViewExtension extends ViewExtensionProvider< - z.infer -> { - override name = 'affine-view-extensions'; - - override schema = optionsSchema; - - override setup( - context: ViewExtensionContext, - options?: z.infer - ) { - super.setup(context); - const { framework, enableAI } = options || {}; - if (framework) { - context.register([ - getThemeExtension(framework), - getFontConfigExtension(), - buildDocDisplayMetaExtension(framework), - ]); - if (context.scope === 'page' || context.scope === 'edgeless') { - context.register(getTelemetryExtension()); - context.register(getEditorConfigExtension(framework)); - context.register(patchFileSizeLimitExtension(framework)); - } - if ( - context.scope === 'preview-page' || - context.scope === 'preview-edgeless' - ) { - context.register(getPagePreviewThemeExtension(framework)); - context.register(patchPeekViewService(framework.get(PeekViewService))); - } - if (enableAI) { - context.register(AIChatBlockSpec); - context.register(AITranscriptionBlockSpec); - context.register( - [ - AICodeBlockWatcher, - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('custom:affine:image'), - config: imageToolbarAIEntryConfig(), - }), - ParagraphBlockConfigExtension({ - getPlaceholder: model => { - const placeholders = { - text: "Type '/' for commands, 'space' for AI", - h1: 'Heading 1', - h2: 'Heading 2', - h3: 'Heading 3', - h4: 'Heading 4', - h5: 'Heading 5', - h6: 'Heading 6', - quote: '', - }; - return placeholders[model.props.type]; - }, - }), - ].flat() - ); - if (context.scope === 'edgeless') { - context.register([ - CopilotTool, - aiPanelWidget, - edgelessCopilotWidget, - getAIEdgelessRootWatcher(framework), - // In note - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('custom:affine:note'), - config: toolbarAIEntryConfig(), - }), - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('custom:affine:surface:*'), - config: edgelessToolbarAIEntryConfig(), - }), - AiSlashMenuConfigExtension(), - ]); - } - if (context.scope === 'page') { - context.register([ - aiPanelWidget, - getAIPageRootWatcher(framework), - ToolbarModuleExtension({ - id: BlockFlavourIdentifier('custom:affine:note'), - config: toolbarAIEntryConfig(), - }), - AiSlashMenuConfigExtension(), - ]); - } - } - } - } -} +import type { FrameworkProvider } from '@toeverything/infra'; const manager = new ViewExtensionManager([ ...getInternalViewExtensions(), - MigratingAffineViewExtension, + AffineCommonViewExtension, + AffineEditorViewExtension, ]); export function getViewManager( framework?: FrameworkProvider, - enableAI?: boolean + enableAI?: boolean, + options?: AffineEditorViewOptions ) { - manager.configure(MigratingAffineViewExtension, { + manager.configure(AffineCommonViewExtension, { framework, enableAI, }); + manager.configure(AffineEditorViewExtension, options); return manager; } diff --git a/packages/frontend/core/src/modules/doc-info/use-std.ts b/packages/frontend/core/src/modules/doc-info/use-std.ts new file mode 100644 index 0000000000..9529b1e742 --- /dev/null +++ b/packages/frontend/core/src/modules/doc-info/use-std.ts @@ -0,0 +1,20 @@ +import { getViewManager } from '@affine/core/blocksuite/manager/migrating-view'; +import { DebugLogger } from '@affine/debug'; +import { BlockStdScope } from '@blocksuite/affine/std'; +import type { Store } from '@blocksuite/affine/store'; +import { useMemo } from 'react'; + +const logger = new DebugLogger('doc-info'); +// todo(pengx17): use rc pool? +export function createBlockStdScope(doc: Store) { + logger.debug('createBlockStdScope', doc.id); + const std = new BlockStdScope({ + store: doc, + extensions: getViewManager().get('page'), + }); + return std; +} + +export function useBlockStdScope(doc: Store) { + return useMemo(() => createBlockStdScope(doc), [doc]); +} diff --git a/packages/frontend/core/src/modules/doc-info/utils.ts b/packages/frontend/core/src/modules/doc-info/utils.ts index d794f35dde..4ed00a6406 100644 --- a/packages/frontend/core/src/modules/doc-info/utils.ts +++ b/packages/frontend/core/src/modules/doc-info/utils.ts @@ -1,12 +1,6 @@ -import { getViewManager } from '@affine/core/blocksuite/manager/migrating-view'; -import { DebugLogger } from '@affine/debug'; -import { BlockStdScope } from '@blocksuite/affine/std'; -import type { Store } from '@blocksuite/affine/store'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Observable } from 'rxjs'; -const logger = new DebugLogger('doc-info'); - interface ReadonlySignal { value: T; subscribe: (fn: (value: T) => void) => () => void; @@ -38,17 +32,3 @@ export function useSignalValue(signal?: ReadonlySignal): T | undefined { }, [signal]); return value; } - -// todo(pengx17): use rc pool? -export function createBlockStdScope(doc: Store) { - logger.debug('createBlockStdScope', doc.id); - const std = new BlockStdScope({ - store: doc, - extensions: getViewManager().get('page'), - }); - return std; -} - -export function useBlockStdScope(doc: Store) { - return useMemo(() => createBlockStdScope(doc), [doc]); -} diff --git a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx index fab1763bdd..c2dc6be1f7 100644 --- a/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx +++ b/packages/frontend/core/src/modules/doc-info/views/database-properties/cells/rich-text.tsx @@ -10,7 +10,7 @@ import { type CSSProperties, useEffect, useRef, useState } from 'react'; import type * as Y from 'yjs'; import type { DatabaseCellRendererProps } from '../../../types'; -import { useBlockStdScope } from '../../../utils'; +import { useBlockStdScope } from '../../../use-std'; import * as styles from './rich-text.css'; // todo(@pengx17): handle markdown/keyboard shortcuts