refactor(editor): improve implementation of lit adapter (#12101)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-01 14:29:11 +00:00
parent e0308c5815
commit 41d404f7f8
26 changed files with 638 additions and 575 deletions

View File

@@ -41,7 +41,6 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
FlavourExtension('affine:code'), FlavourExtension('affine:code'),
CodeBlockHighlighter, CodeBlockHighlighter,
BlockViewExtension('affine:code', literal`affine-code`), BlockViewExtension('affine:code', literal`affine-code`),
codeToolbarWidget,
SlashMenuConfigExtension('affine:code', codeSlashMenuConfig), SlashMenuConfigExtension('affine:code', codeSlashMenuConfig),
CodeKeymapExtension, CodeKeymapExtension,
...getCodeClipboardExtensions(), ...getCodeClipboardExtensions(),
@@ -50,5 +49,8 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
CodeBlockInlineManagerExtension, CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension, CodeBlockUnitSpecExtension,
]); ]);
if (!this.isMobile(context.scope)) {
context.register(codeToolbarWidget);
}
} }
} }

View File

@@ -208,21 +208,21 @@ it('should effect only run once', () => {
const manager = new ViewExtensionManager([ViewExt1]); const manager = new ViewExtensionManager([ViewExt1]);
expect(ViewExt1.effectRunned).toBe(false); expect(ViewExt1.effectRan).toBe(false);
expect(ViewExt2.effectRunned).toBe(false); expect(ViewExt2.effectRan).toBe(false);
manager.get('page'); manager.get('page');
expect(ViewExt1.effectRunned).toBe(true); expect(ViewExt1.effectRan).toBe(true);
expect(ViewExt2.effectRunned).toBe(false); expect(ViewExt2.effectRan).toBe(false);
expect(effect1).toHaveBeenCalledTimes(1); expect(effect1).toHaveBeenCalledTimes(1);
expect(effect2).toHaveBeenCalledTimes(0); expect(effect2).toHaveBeenCalledTimes(0);
manager.get('edgeless'); manager.get('edgeless');
expect(ViewExt1.effectRunned).toBe(true); expect(ViewExt1.effectRan).toBe(true);
expect(ViewExt2.effectRunned).toBe(false); expect(ViewExt2.effectRan).toBe(false);
expect(effect1).toHaveBeenCalledTimes(1); expect(effect1).toHaveBeenCalledTimes(1);
expect(effect2).toHaveBeenCalledTimes(0); expect(effect2).toHaveBeenCalledTimes(0);

View File

@@ -64,7 +64,7 @@ export class ViewExtensionProvider<
* Static flag to ensure effect is only run once per provider class * Static flag to ensure effect is only run once per provider class
* @internal * @internal
*/ */
static effectRunned = false; static effectRan = false;
/** /**
* Override this method to implement one-time initialization logic for the provider. * 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) { override setup(context: ViewExtensionContext, options?: Options) {
super.setup(context, options); super.setup(context, options);
const constructer = this.constructor as typeof ViewExtensionProvider; const constructor = this.constructor as typeof ViewExtensionProvider;
if (!constructer.effectRunned) { if (!constructor.effectRan) {
constructer.effectRunned = true; constructor.effectRan = true;
this.effect(); this.effect();
} }
} }

View File

@@ -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,
];

View File

@@ -3,14 +3,6 @@ import {
type ViewExtensionContext, type ViewExtensionContext,
ViewExtensionProvider, ViewExtensionProvider,
} from '@blocksuite/affine-ext-loader'; } from '@blocksuite/affine-ext-loader';
import {
AttachmentAdapter,
ClipboardAdapter,
HtmlAdapter,
ImageAdapter,
MixTextAdapter,
NotionTextAdapter,
} from '@blocksuite/affine-shared/adapters';
import { import {
AutoClearSelectionService, AutoClearSelectionService,
DefaultOpenDocExtension, DefaultOpenDocExtension,
@@ -25,67 +17,11 @@ import {
ThemeService, ThemeService,
ToolbarRegistryExtension, ToolbarRegistryExtension,
} from '@blocksuite/affine-shared/services'; } from '@blocksuite/affine-shared/services';
import { ClipboardAdapterConfigExtension } from '@blocksuite/std';
import { InteractivityManager, ToolController } from '@blocksuite/std/gfx'; import { InteractivityManager, ToolController } from '@blocksuite/std/gfx';
import type { ExtensionType } from '@blocksuite/store';
import { clipboardConfigs } from './clipboard';
import { effects } from './effects'; 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 { export class FoundationViewExtension extends ViewExtensionProvider {
override name = 'foundation'; override name = 'foundation';

View File

@@ -6,7 +6,7 @@ import {
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { type Container, createIdentifier } from '@blocksuite/global/di'; import { type Container, createIdentifier } from '@blocksuite/global/di';
import { type BlockStdScope, StdIdentifier } from '@blocksuite/std'; 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 Signal, signal } from '@preact/signals-core';
import { import {
type AffineCssVariables, type AffineCssVariables,
@@ -25,14 +25,6 @@ export interface ThemeExtension {
getEdgelessTheme?: (docId?: string) => Signal<ColorScheme>; getEdgelessTheme?: (docId?: string) => Signal<ColorScheme>;
} }
export function OverrideThemeExtension(service: ThemeExtension): ExtensionType {
return {
setup: di => {
di.override(ThemeExtensionIdentifier, () => service);
},
};
}
export const ThemeProvider = createIdentifier<ThemeService>( export const ThemeProvider = createIdentifier<ThemeService>(
'AffineThemeProvider' 'AffineThemeProvider'
); );
@@ -78,6 +70,7 @@ export class ThemeService extends Extension {
* *
* @param color - A color value. * @param color - A color value.
* @param fallback - If color value processing fails, it will be used as a fallback. * @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. * @returns - A color property string.
* *
* @example * @example
@@ -112,6 +105,7 @@ export class ThemeService extends Extension {
* @param color - A color value. * @param color - A color value.
* @param fallback - If color value processing fails, it will be used as a fallback. * @param fallback - If color value processing fails, it will be used as a fallback.
* @param real - If true, it returns the computed style. * @param real - If true, it returns the computed style.
* @param theme - Target theme, default is the current theme.
* @returns - A color property string. * @returns - A color property string.
* *
* @example * @example

View File

@@ -16,6 +16,7 @@ export class SlashMenuViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) { override setup(context: ViewExtensionContext) {
super.setup(context); super.setup(context);
if (this.isMobile(context.scope)) return;
context.register(SlashMenuExtension); context.register(SlashMenuExtension);
} }
} }

View File

@@ -16,6 +16,7 @@ export class ToolbarViewExtension extends ViewExtensionProvider {
override setup(context: ViewExtensionContext) { override setup(context: ViewExtensionContext) {
super.setup(context); super.setup(context);
if (this.isMobile(context.scope)) return;
context.register(toolbarWidget); context.register(toolbarWidget);
} }
} }

View File

@@ -1,5 +1,6 @@
export { export {
type ElementOrFactory, type ElementOrFactory,
type ReactToLit,
useLitPortal, useLitPortal,
useLitPortalFactory, useLitPortalFactory,
} from './lit-portal'; } from './lit-portal';

View File

@@ -1,4 +1,4 @@
import { html, LitElement } from 'lit'; import { html, LitElement, type TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js'; import { customElement } from 'lit/decorators.js';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
@@ -82,59 +82,60 @@ type LitPortal = {
litElement: LitReactPortal; litElement: LitReactPortal;
}; };
export type ReactToLit = (
elementOrFactory: ElementOrFactory,
rerendering?: boolean
) => TemplateResult;
// returns a factory function that renders a given element to a lit template // returns a factory function that renders a given element to a lit template
export const useLitPortalFactory = () => { export const useLitPortalFactory = () => {
const [portals, setPortals] = useState<LitPortal[]>([]); const [portals, setPortals] = useState<LitPortal[]>([]);
return [ const reactToLit: ReactToLit = useCallback(
useCallback( (elementOrFactory, rerendering) => {
( const element =
elementOrFactory: React.ReactElement | (() => React.ReactElement), typeof elementOrFactory === 'function'
rerendering = true ? elementOrFactory()
) => { : elementOrFactory;
const element = return createLitPortalAnchor(event => {
typeof elementOrFactory === 'function' setPortals(portals => {
? elementOrFactory() const { name, target } = event;
: elementOrFactory; const id = target.portalId;
return createLitPortalAnchor(event => { let newPortals = portals;
setPortals(portals => { const updatePortals = () => {
const { name, target } = event; let oldPortalIndex = portals.findIndex(
const id = target.portalId; p => p.litElement === target
let newPortals = portals; );
const updatePortals = () => { oldPortalIndex =
let oldPortalIndex = portals.findIndex( oldPortalIndex === -1 ? portals.length : oldPortalIndex;
p => p.litElement === target newPortals = portals.toSpliced(oldPortalIndex, 1, {
); id,
oldPortalIndex = portal: ReactDOM.createPortal(element, target),
oldPortalIndex === -1 ? portals.length : oldPortalIndex; litElement: target,
newPortals = portals.toSpliced(oldPortalIndex, 1, { });
id, };
portal: ReactDOM.createPortal(element, target), switch (name) {
litElement: target, case 'connectedCallback':
}); updatePortals();
}; break;
switch (name) { case 'disconnectedCallback':
case 'connectedCallback': newPortals = portals.filter(p => p.litElement.isConnected);
updatePortals(); break;
case 'willUpdate':
if (!target.isConnected || !rerendering) {
break; break;
case 'disconnectedCallback': }
newPortals = portals.filter(p => p.litElement.isConnected); updatePortals();
break; break;
case 'willUpdate': }
if (!target.isConnected || !rerendering) { return newPortals;
break;
}
updatePortals();
break;
}
return newPortals;
});
}); });
}, });
[setPortals] },
), []
portals, );
] as const;
return [reactToLit, portals] as const;
}; };
// render a react element to a lit template // render a react element to a lit template

View File

@@ -6,22 +6,17 @@ import {
LitEdgelessEditor, LitEdgelessEditor,
type PageEditor, type PageEditor,
} from '@affine/core/blocksuite/editors'; } 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 { 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 type { DocCustomPropertyInfo } from '@affine/core/modules/db';
import { DocService, DocsService } from '@affine/core/modules/doc';
import type { import type {
DatabaseRow, DatabaseRow,
DatabaseValueCell, DatabaseValueCell,
} from '@affine/core/modules/doc-info/types'; } from '@affine/core/modules/doc-info/types';
import { EditorService } from '@affine/core/modules/editor';
import { EditorSettingService } from '@affine/core/modules/editor-setting'; import { EditorSettingService } from '@affine/core/modules/editor-setting';
import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { JournalService } from '@affine/core/modules/journal'; import { JournalService } from '@affine/core/modules/journal';
import { toDocSearchParams } from '@affine/core/modules/navigation';
import { useInsidePeekView } from '@affine/core/modules/peek-view'; 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 { WorkspaceService } from '@affine/core/modules/workspace';
import track from '@affine/track'; import track from '@affine/track';
import type { DocTitle } from '@blocksuite/affine/fragments/doc-title'; import type { DocTitle } from '@blocksuite/affine/fragments/doc-title';
@@ -43,41 +38,11 @@ import {
useRef, useRef,
} from 'react'; } from 'react';
import {
AffinePageReference,
AffineSharedPageReference,
} from '../../components/affine/reference-link';
import { import {
type DefaultOpenProperty, type DefaultOpenProperty,
DocPropertiesTable, DocPropertiesTable,
} from '../../components/doc-properties'; } 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 { 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 { 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';
@@ -92,55 +57,12 @@ interface BlocksuiteEditorProps {
const usePatchSpecs = (mode: DocMode) => { const usePatchSpecs = (mode: DocMode) => {
const [reactToLit, portals] = useLitPortalFactory(); const [reactToLit, portals] = useLitPortalFactory();
const { const { workspaceService, featureFlagService } = useServices({
peekViewService,
docService,
docsService,
editorService,
workspaceService,
featureFlagService,
memberSearchService,
publicUserService,
} = useServices({
PeekViewService,
DocService,
DocsService,
WorkspaceService, WorkspaceService,
EditorService,
FeatureFlagService, FeatureFlagService,
MemberSearchService,
PublicUserService,
}); });
const isCloud = workspaceService.workspace.flavour !== 'local'; const isCloud = workspaceService.workspace.flavour !== 'local';
const framework = useFramework(); const framework = useFramework();
const referenceRenderer: ReferenceReactRenderer = useMemo(() => {
return function customReference(reference) {
const data = reference.delta.attributes?.reference;
if (!data) return <span />;
const pageId = data.pageId;
if (!pageId) return <span />;
// title alias
const title = data.title;
const params = toDocSearchParams(data.params);
if (workspaceService.workspace.openOptions.isSharedMode) {
return (
<AffineSharedPageReference
docCollection={workspaceService.workspace.docCollection}
pageId={pageId}
params={params}
title={title}
/>
);
}
return (
<AffinePageReference pageId={pageId} params={params} title={title} />
);
};
}, [workspaceService]);
const confirmModal = useConfirmModal(); const confirmModal = useConfirmModal();
@@ -158,70 +80,33 @@ const usePatchSpecs = (mode: DocMode) => {
) )
); );
const patchedSpecs = useMemo(() => { const editorOptions: AffineEditorViewOptions = useMemo(() => {
let extensions = enableEditorExtension(framework, mode, enableAI); return {
isCloud,
isInPeekView: insidePeekView,
extensions = extensions.concat( enableTurboRenderer,
[ enablePDFEmbedPreview,
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()
);
if (enablePDFEmbedPreview) { framework,
extensions = extensions.concat([patchForPDFEmbedView(reactToLit)]);
}
if (BUILD_CONFIG.isMobileEdition) { reactToLit: reactToLit as AffineEditorViewOptions['reactToLit'],
extensions = enableMobileExtension(extensions, framework); confirmModal,
} };
if (BUILD_CONFIG.isElectron) {
extensions = extensions.concat(
[patchForClipboardInElectron(framework)].flat()
);
}
return extensions;
}, [ }, [
framework,
mode,
enableAI,
reactToLit,
referenceRenderer,
insidePeekView,
confirmModal, confirmModal,
peekViewService,
docService,
docsService,
editorService,
isCloud,
memberSearchService,
publicUserService,
enableTurboRenderer,
enablePDFEmbedPreview, enablePDFEmbedPreview,
enableTurboRenderer,
framework,
insidePeekView,
isCloud,
reactToLit,
]); ]);
const patchedSpecs = useMemo(() => {
return enableEditorExtension(framework, mode, enableAI, editorOptions);
}, [framework, mode, enableAI, editorOptions]);
return [ return [
patchedSpecs, patchedSpecs,
useMemo( useMemo(

View File

@@ -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 { AttachmentEmbedPreview } from '@affine/core/blocksuite/attachment-viewer/attachment-embed-preview';
import { AttachmentEmbedConfigIdentifier } from '@blocksuite/affine/blocks/attachment'; import { AttachmentEmbedConfigIdentifier } from '@blocksuite/affine/blocks/attachment';
import { Bound } from '@blocksuite/affine/global/gfx'; import { Bound } from '@blocksuite/affine/global/gfx';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { TemplateResult } from 'lit';
export function patchForPDFEmbedView( export function patchForPDFEmbedView(reactToLit: ReactToLit): ExtensionType {
reactToLit: (
element: ElementOrFactory,
rerendering?: boolean
) => TemplateResult
): ExtensionType {
return { return {
setup: di => { setup: di => {
di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({ di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({
@@ -35,12 +29,7 @@ export function patchForPDFEmbedView(
}; };
} }
export function patchForAudioEmbedView( export function patchForAudioEmbedView(reactToLit: ReactToLit): ExtensionType {
reactToLit: (
element: ElementOrFactory,
rerendering?: boolean
) => TemplateResult
): ExtensionType {
return { return {
setup: di => { setup: di => {
di.override(AttachmentEmbedConfigIdentifier('audio'), () => ({ di.override(AttachmentEmbedConfigIdentifier('audio'), () => ({

View File

@@ -1,4 +1,4 @@
import type { ElementOrFactory } from '@affine/component'; import type { ReactToLit } from '@affine/component';
import { JournalService } from '@affine/core/modules/journal'; import { JournalService } from '@affine/core/modules/journal';
import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc'; import { EmbedSyncedDocConfigExtension } from '@blocksuite/affine/blocks/embed-doc';
import { NoteConfigExtension } from '@blocksuite/affine/blocks/note'; import { NoteConfigExtension } from '@blocksuite/affine/blocks/note';
@@ -10,7 +10,7 @@ import {
} from '@blocksuite/affine/shared/services'; } from '@blocksuite/affine/shared/services';
import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/affine/std/gfx';
import type { FrameworkProvider } from '@toeverything/infra'; 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 { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title';
import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header'; import { EdgelessEmbedSyncedDocHeader } from './edgeless-embed-synced-doc-header';
@@ -18,7 +18,7 @@ import { EdgelessNoteHeader } from './edgeless-note-header';
export function patchForEdgelessNoteConfig( export function patchForEdgelessNoteConfig(
framework: FrameworkProvider, framework: FrameworkProvider,
reactToLit: (element: ElementOrFactory) => TemplateResult, reactToLit: ReactToLit,
insidePeekView: boolean insidePeekView: boolean
) { ) {
return NoteConfigExtension({ return NoteConfigExtension({
@@ -100,9 +100,7 @@ export function patchForEdgelessNoteConfig(
}); });
} }
export function patchForEmbedSyncedDocConfig( export function patchForEmbedSyncedDocConfig(reactToLit: ReactToLit) {
reactToLit: (element: ElementOrFactory) => TemplateResult
) {
return EmbedSyncedDocConfigExtension({ return EmbedSyncedDocConfigExtension({
edgelessHeader: ({ model, std }) => edgelessHeader: ({ model, std }) =>
reactToLit(<EdgelessEmbedSyncedDocHeader model={model} std={std} />), reactToLit(<EdgelessEmbedSyncedDocHeader model={model} std={std} />),

View File

@@ -1,3 +1,4 @@
import type { AffineEditorViewOptions } from '@affine/core/blocksuite/manager/editor-view';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import { type FrameworkProvider } from '@toeverything/infra'; import { type FrameworkProvider } from '@toeverything/infra';
@@ -6,8 +7,16 @@ import { getViewManager } from '../../manager/migrating-view';
export function enableEditorExtension( export function enableEditorExtension(
framework: FrameworkProvider, framework: FrameworkProvider,
mode: 'edgeless' | 'page', mode: 'edgeless' | 'page',
enableAI: boolean enableAI: boolean,
options: AffineEditorViewOptions
): ExtensionType[] { ): 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); return manager.get(mode);
} }

View File

@@ -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<ColorScheme>;
readonly disposables: (() => void)[] = [];
static override setup(di: Container) {
super.setup(di);
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
StdIdentifier,
]);
}
constructor(std: BlockStdScope) {
super(std);
const theme$: Observable<ColorScheme> = framework
.get(AppThemeService)
.appTheme.theme$.map(theme => {
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
});
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
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;
}

View File

@@ -1 +0,0 @@
export * from './entry/enable-mobile';

View File

@@ -1,8 +1,5 @@
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard'; import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
import { import { CodeBlockConfigExtension } from '@blocksuite/affine/blocks/code';
CodeBlockConfigExtension,
codeToolbarWidget,
} from '@blocksuite/affine/blocks/code';
import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph'; import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph';
import type { Container } from '@blocksuite/affine/global/di'; import type { Container } from '@blocksuite/affine/global/di';
import { DisposableGroup } from '@blocksuite/affine/global/disposable'; import { DisposableGroup } from '@blocksuite/affine/global/disposable';
@@ -13,12 +10,10 @@ import {
} from '@blocksuite/affine/shared/services'; } from '@blocksuite/affine/shared/services';
import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std'; import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store'; 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 { batch, signal } from '@preact/signals-core';
import type { FrameworkProvider } from '@toeverything/infra'; import type { FrameworkProvider } from '@toeverything/infra';
class MobileSpecsPatches extends LifeCycleWatcher { export class MobileSpecsPatches extends LifeCycleWatcher {
static override key = 'mobile-patches'; static override key = 'mobile-patches';
constructor(std: BlockStdScope) { constructor(std: BlockStdScope) {
@@ -30,7 +25,7 @@ class MobileSpecsPatches extends LifeCycleWatcher {
} }
} }
const mobileParagraphConfig = ParagraphBlockConfigExtension({ export const mobileParagraphConfig = ParagraphBlockConfigExtension({
getPlaceholder: model => { getPlaceholder: model => {
const placeholders = { const placeholders = {
text: '', text: '',
@@ -46,11 +41,13 @@ const mobileParagraphConfig = ParagraphBlockConfigExtension({
}, },
}); });
const mobileCodeConfig = CodeBlockConfigExtension({ export const mobileCodeConfig = CodeBlockConfigExtension({
showLineNumbers: false, showLineNumbers: false,
}); });
function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType { export function KeyboardToolbarExtension(
framework: FrameworkProvider
): ExtensionType {
const affineVirtualKeyboardProvider = framework.get(VirtualKeyboardProvider); const affineVirtualKeyboardProvider = framework.get(VirtualKeyboardProvider);
class BSVirtualKeyboardService class BSVirtualKeyboardService
@@ -109,28 +106,3 @@ function KeyboardToolbarExtension(framework: FrameworkProvider): ExtensionType {
return BSVirtualKeyboardService; 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;
}

View File

@@ -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 type { AffineReference } from '@blocksuite/affine/inlines/reference';
import { ReferenceNodeConfigExtension } from '@blocksuite/affine/inlines/reference'; import { ReferenceNodeConfigExtension } from '@blocksuite/affine/inlines/reference';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
import type { TemplateResult } from 'lit';
export type ReferenceReactRenderer = ( export type ReferenceReactRenderer = (
reference: AffineReference reference: AffineReference
) => React.ReactElement; ) => React.ReactElement;
export function patchReferenceRenderer( export function patchReferenceRenderer(
reactToLit: (element: ElementOrFactory) => TemplateResult, reactToLit: ReactToLit,
reactRenderer: ReferenceReactRenderer reactRenderer: ReferenceReactRenderer
): ExtensionType { ): ExtensionType {
const customContent = (reference: AffineReference) => { const customContent = (reference: AffineReference) => {
const node = reactRenderer(reference); const node = reactRenderer(reference);
return reactToLit(node); return reactToLit(node, true);
}; };
return ReferenceNodeConfigExtension({ return ReferenceNodeConfigExtension({

View File

@@ -10,7 +10,11 @@ import {
createSignalFromObservable, createSignalFromObservable,
type Signal, type Signal,
} from '@blocksuite/affine/shared/utils'; } 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 FrameworkProvider } from '@toeverything/infra';
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import { combineLatest, map } from 'rxjs'; import { combineLatest, map } from 'rxjs';
@@ -95,3 +99,58 @@ export function getThemeExtension(
return AffineThemeExtension; return AffineThemeExtension;
} }
export function getPreviewThemeExtension(framework: FrameworkProvider) {
class AffinePagePreviewThemeExtension
extends LifeCycleWatcher
implements ThemeExtension
{
static override readonly key = 'affine-page-preview-theme';
readonly theme: Signal<ColorScheme>;
readonly disposables: (() => void)[] = [];
static override setup(di: Container) {
super.setup(di);
di.override(ThemeExtensionIdentifier, AffinePagePreviewThemeExtension, [
StdIdentifier,
]);
}
constructor(std: BlockStdScope) {
super(std);
const theme$: Observable<ColorScheme> = framework
.get(AppThemeService)
.appTheme.theme$.map(theme => {
return theme === ColorScheme.Dark
? ColorScheme.Dark
: ColorScheme.Light;
});
const { signal, cleanup } = createSignalFromObservable<ColorScheme>(
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;
}

View File

@@ -14,20 +14,18 @@ function createPainterWorker() {
return worker; return worker;
} }
export function patchTurboRendererExtension() { export const turboRendererExtension = [
return [ ParagraphLayoutHandlerExtension,
ParagraphLayoutHandlerExtension, ListLayoutHandlerExtension,
ListLayoutHandlerExtension, NoteLayoutHandlerExtension,
NoteLayoutHandlerExtension, CodeLayoutHandlerExtension,
CodeLayoutHandlerExtension, ImageLayoutHandlerExtension,
ImageLayoutHandlerExtension, TurboRendererConfigFactory({
TurboRendererConfigFactory({ options: {
options: { zoomThreshold: 1,
zoomThreshold: 1, debounceTime: 1000,
debounceTime: 1000, },
}, painterWorkerEntry: createPainterWorker,
painterWorkerEntry: createPainterWorker, }),
}), ViewportTurboRendererExtension,
ViewportTurboRendererExtension, ];
];
}

View File

@@ -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<typeof optionsSchema>
> {
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<typeof optionsSchema>
) {
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);
}
}
}
}

View File

@@ -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<ElementOrFactory>(), z.boolean().optional())
.returns(z.custom<TemplateResult>()),
confirmModal: z.object({
openConfirmModal: z
.function()
.args(z.custom<ConfirmModalProps>().optional(), z.any().optional()),
closeConfirmModal: z.function(),
}),
});
export type AffineEditorViewOptions = z.infer<typeof optionsSchema>;
export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEditorViewOptions> {
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 <span />;
const pageId = data.pageId;
if (!pageId) return <span />;
// title alias
const title = data.title;
const params = toDocSearchParams(data.params);
if (workspaceService.workspace.openOptions.isSharedMode) {
return (
<AffineSharedPageReference
docCollection={workspaceService.workspace.docCollection}
pageId={pageId}
params={params}
title={title}
/>
);
}
return (
<AffinePageReference pageId={pageId} params={params} title={title} />
);
};
};
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));
}
}
}

View File

@@ -1,147 +1,28 @@
import { PeekViewService } from '@affine/core/modules/peek-view'; import { AffineCommonViewExtension } from '@affine/core/blocksuite/manager/common-view';
import { ParagraphBlockConfigExtension } from '@blocksuite/affine/blocks/paragraph';
import { import {
type ViewExtensionContext, AffineEditorViewExtension,
ViewExtensionManager, type AffineEditorViewOptions,
ViewExtensionProvider, } from '@affine/core/blocksuite/manager/editor-view';
} from '@blocksuite/affine/ext-loader'; import { ViewExtensionManager } from '@blocksuite/affine/ext-loader';
import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view'; import { getInternalViewExtensions } from '@blocksuite/affine/extensions/view';
import { ToolbarModuleExtension } from '@blocksuite/affine/shared/services'; import type { FrameworkProvider } from '@toeverything/infra';
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<typeof optionsSchema>
> {
override name = 'affine-view-extensions';
override schema = optionsSchema;
override setup(
context: ViewExtensionContext,
options?: z.infer<typeof optionsSchema>
) {
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(),
]);
}
}
}
}
}
const manager = new ViewExtensionManager([ const manager = new ViewExtensionManager([
...getInternalViewExtensions(), ...getInternalViewExtensions(),
MigratingAffineViewExtension, AffineCommonViewExtension,
AffineEditorViewExtension,
]); ]);
export function getViewManager( export function getViewManager(
framework?: FrameworkProvider, framework?: FrameworkProvider,
enableAI?: boolean enableAI?: boolean,
options?: AffineEditorViewOptions
) { ) {
manager.configure(MigratingAffineViewExtension, { manager.configure(AffineCommonViewExtension, {
framework, framework,
enableAI, enableAI,
}); });
manager.configure(AffineEditorViewExtension, options);
return manager; return manager;
} }

View File

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

View File

@@ -1,12 +1,6 @@
import { getViewManager } from '@affine/core/blocksuite/manager/migrating-view'; import { useEffect, useState } from 'react';
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 { Observable } from 'rxjs'; import { Observable } from 'rxjs';
const logger = new DebugLogger('doc-info');
interface ReadonlySignal<T> { interface ReadonlySignal<T> {
value: T; value: T;
subscribe: (fn: (value: T) => void) => () => void; subscribe: (fn: (value: T) => void) => () => void;
@@ -38,17 +32,3 @@ export function useSignalValue<T>(signal?: ReadonlySignal<T>): T | undefined {
}, [signal]); }, [signal]);
return value; 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]);
}

View File

@@ -10,7 +10,7 @@ import { type CSSProperties, useEffect, useRef, useState } from 'react';
import type * as Y from 'yjs'; import type * as Y from 'yjs';
import type { DatabaseCellRendererProps } from '../../../types'; import type { DatabaseCellRendererProps } from '../../../types';
import { useBlockStdScope } from '../../../utils'; import { useBlockStdScope } from '../../../use-std';
import * as styles from './rich-text.css'; import * as styles from './rich-text.css';
// todo(@pengx17): handle markdown/keyboard shortcuts // todo(@pengx17): handle markdown/keyboard shortcuts