mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-17 10:06:17 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<ColorScheme>;
|
||||
}
|
||||
|
||||
export function OverrideThemeExtension(service: ThemeExtension): ExtensionType {
|
||||
return {
|
||||
setup: di => {
|
||||
di.override(ThemeExtensionIdentifier, () => service);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ThemeProvider = createIdentifier<ThemeService>(
|
||||
'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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {
|
||||
type ElementOrFactory,
|
||||
type ReactToLit,
|
||||
useLitPortal,
|
||||
useLitPortalFactory,
|
||||
} from './lit-portal';
|
||||
|
||||
@@ -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<LitPortal[]>([]);
|
||||
|
||||
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
|
||||
|
||||
@@ -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 <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();
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'), () => ({
|
||||
|
||||
@@ -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(<EdgelessEmbedSyncedDocHeader model={model} std={std} />),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './entry/enable-mobile';
|
||||
+7
-35
@@ -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;
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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<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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
@@ -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<T> {
|
||||
value: T;
|
||||
subscribe: (fn: (value: T) => void) => () => void;
|
||||
@@ -38,17 +32,3 @@ export function useSignalValue<T>(signal?: ReadonlySignal<T>): 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]);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user