refactor(editor): extract mobile extension builder (#12239)

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced a new mobile view extension that activates mobile-specific UI features based on the runtime environment.
  - Automatically enables mobile keyboard toolbar and linked document menu features in mobile contexts.

- **Improvements**
  - Simplified code and paragraph block configurations on mobile, including disabling line numbers and adjusting placeholders.
  - Enhanced configuration chaining to include mobile-specific settings by default.
  - Improved extension registration flow with method chaining support.

- **Refactor**
  - Removed deprecated mobile patch classes and configurations, consolidating mobile logic into dedicated extensions.
  - Streamlined mobile-related code for better maintainability and performance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Saul-Mirone
2025-05-12 12:54:51 +00:00
parent 0464e03b92
commit cb550b7b21
11 changed files with 128 additions and 72 deletions

View File

@@ -11,6 +11,7 @@ import {
import { literal, unsafeStatic } from 'lit/static-html.js'; import { literal, unsafeStatic } from 'lit/static-html.js';
import { getCodeClipboardExtensions } from './clipboard/index.js'; import { getCodeClipboardExtensions } from './clipboard/index.js';
import { CodeBlockConfigExtension } from './code-block-config';
import { import {
CodeBlockInlineManagerExtension, CodeBlockInlineManagerExtension,
CodeBlockUnitSpecExtension, CodeBlockUnitSpecExtension,
@@ -21,7 +22,7 @@ import { AFFINE_CODE_TOOLBAR_WIDGET } from './code-toolbar/index.js';
import { codeSlashMenuConfig } from './configs/slash-menu.js'; import { codeSlashMenuConfig } from './configs/slash-menu.js';
import { effects } from './effects.js'; import { effects } from './effects.js';
export const codeToolbarWidget = WidgetViewExtension( const codeToolbarWidget = WidgetViewExtension(
'affine:code', 'affine:code',
AFFINE_CODE_TOOLBAR_WIDGET, AFFINE_CODE_TOOLBAR_WIDGET,
literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}` literal`${unsafeStatic(AFFINE_CODE_TOOLBAR_WIDGET)}`
@@ -51,6 +52,12 @@ export class CodeBlockViewExtension extends ViewExtensionProvider {
]); ]);
if (!this.isMobile(context.scope)) { if (!this.isMobile(context.scope)) {
context.register(codeToolbarWidget); context.register(codeToolbarWidget);
} else {
context.register(
CodeBlockConfigExtension({
showLineNumbers: false,
})
);
} }
} }
} }

View File

@@ -17,7 +17,7 @@ export type Context<Scope extends string> = {
/** The scope this context is associated with */ /** The scope this context is associated with */
scope: Scope; scope: Scope;
/** Function to register one or more extensions */ /** Function to register one or more extensions */
register(extensions: ExtensionType[] | ExtensionType): void; register(extensions: ExtensionType[] | ExtensionType): Context<Scope>;
}; };
/** /**

View File

@@ -75,11 +75,14 @@ export class ExtensionManager<Scope extends string> {
/** @internal */ /** @internal */
private readonly _getContextByScope = (scope: Scope): Context<Scope> => { private readonly _getContextByScope = (scope: Scope): Context<Scope> => {
return { const context: Context<Scope> = {
scope, scope,
register: (extensions: ExtensionType[] | ExtensionType) => register: (extensions: ExtensionType[] | ExtensionType) => {
this._registerToScope(scope, extensions), this._registerToScope(scope, extensions);
return context;
},
}; };
return context;
}; };
/** /**

View File

@@ -98,7 +98,8 @@ const usePatchSpecs = (mode: DocMode) => {
}) })
.database(framework) .database(framework)
.linkedDoc(framework) .linkedDoc(framework)
.paragraph(enableAI).value; .paragraph(enableAI)
.mobile(framework).value;
if (BUILD_CONFIG.isMobileEdition) { if (BUILD_CONFIG.isMobileEdition) {
if (mode === 'page') { if (mode === 'page') {

View File

@@ -0,0 +1,33 @@
import { KeyboardToolbarExtension } from '@affine/core/blocksuite/extensions/mobile/keyboard-toolbar-extension';
import { MobileFeatureFlagControl } from '@affine/core/blocksuite/extensions/mobile/mobile-feature-flag-control';
import {
type ViewExtensionContext,
ViewExtensionProvider,
} from '@blocksuite/affine/ext-loader';
import { FrameworkProvider } from '@toeverything/infra';
import { z } from 'zod';
const optionsSchema = z.object({
framework: z.instanceof(FrameworkProvider).optional(),
});
type MobileViewOptions = z.infer<typeof optionsSchema>;
export class MobileViewExtension extends ViewExtensionProvider<MobileViewOptions> {
override name = 'mobile-view-extension';
override schema = optionsSchema;
override setup(context: ViewExtensionContext, options?: MobileViewOptions) {
super.setup(context, options);
const isMobile = BUILD_CONFIG.isMobileEdition;
if (!isMobile) return;
const framework = options?.framework;
if (framework) {
context.register(KeyboardToolbarExtension(framework));
}
context.register(MobileFeatureFlagControl);
}
}

View File

@@ -1,50 +1,15 @@
import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard'; import { VirtualKeyboardProvider } from '@affine/core/mobile/modules/virtual-keyboard';
import { CodeBlockConfigExtension } from '@blocksuite/affine/blocks/code';
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';
import { import {
FeatureFlagService,
VirtualKeyboardProvider as BSVirtualKeyboardProvider, VirtualKeyboardProvider as BSVirtualKeyboardProvider,
type VirtualKeyboardProviderWithAction, type VirtualKeyboardProviderWithAction,
} from '@blocksuite/affine/shared/services'; } from '@blocksuite/affine/shared/services';
import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std'; import { LifeCycleWatcher } from '@blocksuite/affine/std';
import type { ExtensionType } from '@blocksuite/affine/store'; import type { ExtensionType } from '@blocksuite/affine/store';
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';
export class MobileSpecsPatches extends LifeCycleWatcher {
static override key = 'mobile-patches';
constructor(std: BlockStdScope) {
super(std);
const featureFlagService = std.get(FeatureFlagService);
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
}
}
export const mobileParagraphConfig = ParagraphBlockConfigExtension({
getPlaceholder: model => {
const placeholders = {
text: '',
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
quote: '',
};
return placeholders[model.props.type];
},
});
export const mobileCodeConfig = CodeBlockConfigExtension({
showLineNumbers: false,
});
export function KeyboardToolbarExtension( export function KeyboardToolbarExtension(
framework: FrameworkProvider framework: FrameworkProvider
): ExtensionType { ): ExtensionType {
@@ -89,6 +54,7 @@ export function KeyboardToolbarExtension(
if ('show' in affineVirtualKeyboardProvider) { if ('show' in affineVirtualKeyboardProvider) {
const providerWithAction = affineVirtualKeyboardProvider; const providerWithAction = affineVirtualKeyboardProvider;
class BSVirtualKeyboardServiceWithShowAndHide class BSVirtualKeyboardServiceWithShowAndHide
extends BSVirtualKeyboardService extends BSVirtualKeyboardService
implements VirtualKeyboardProviderWithAction implements VirtualKeyboardProviderWithAction
@@ -96,6 +62,7 @@ export function KeyboardToolbarExtension(
show() { show() {
providerWithAction.show(); providerWithAction.show();
} }
hide() { hide() {
providerWithAction.hide(); providerWithAction.hide();
} }

View File

@@ -0,0 +1,14 @@
import { FeatureFlagService } from '@blocksuite/affine/shared/services';
import { type BlockStdScope, LifeCycleWatcher } from '@blocksuite/affine/std';
export class MobileFeatureFlagControl extends LifeCycleWatcher {
static override key = 'mobile-patches';
constructor(std: BlockStdScope) {
super(std);
const featureFlagService = std.get(FeatureFlagService);
featureFlagService.setFlag('enable_mobile_keyboard_toolbar', true);
featureFlagService.setFlag('enable_mobile_linked_doc_menu', true);
}
}

View File

@@ -11,6 +11,7 @@ import { CopilotTool } from '@affine/core/blocksuite/ai/tool/copilot-tool';
import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel'; import { aiPanelWidget } from '@affine/core/blocksuite/ai/widgets/ai-panel/ai-panel';
import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot'; import { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot';
import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta'; import { buildDocDisplayMetaExtension } from '@affine/core/blocksuite/extensions/display-meta';
import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard';
import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit'; import { patchFileSizeLimitExtension } from '@affine/core/blocksuite/extensions/file-size-limit';
import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config'; import { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config';
import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service'; import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service';
@@ -41,15 +42,17 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
context: ViewExtensionContext, context: ViewExtensionContext,
framework: FrameworkProvider framework: FrameworkProvider
) { ) {
context.register(AIChatBlockSpec); context
context.register(AITranscriptionBlockSpec); .register(AIChatBlockSpec)
context.register([ .register(AITranscriptionBlockSpec)
AICodeBlockWatcher, .register(EdgelessClipboardAIChatConfig)
ToolbarModuleExtension({ .register(AICodeBlockWatcher)
id: BlockFlavourIdentifier('custom:affine:image'), .register(
config: imageToolbarAIEntryConfig(), ToolbarModuleExtension({
}), id: BlockFlavourIdentifier('custom:affine:image'),
]); config: imageToolbarAIEntryConfig(),
})
);
if (context.scope === 'edgeless' || context.scope === 'page') { if (context.scope === 'edgeless' || context.scope === 'page') {
context.register([ context.register([
aiPanelWidget, aiPanelWidget,

View File

@@ -3,7 +3,6 @@ import { patchForAudioEmbedView } from '@affine/core/blocksuite/extensions/audio
import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service'; import { patchDatabaseBlockConfigService } from '@affine/core/blocksuite/extensions/database-block-config-service';
import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service'; import { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service';
import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url'; import { patchDocUrlExtensions } from '@affine/core/blocksuite/extensions/doc-url';
import { EdgelessClipboardAIChatConfig } from '@affine/core/blocksuite/extensions/edgeless-clipboard';
import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard'; import { patchForClipboardInElectron } from '@affine/core/blocksuite/extensions/electron-clipboard';
import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service'; import { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service';
import { patchOpenDocExtension } from '@affine/core/blocksuite/extensions/open-doc'; import { patchOpenDocExtension } from '@affine/core/blocksuite/extensions/open-doc';
@@ -29,13 +28,6 @@ import { FrameworkProvider } from '@toeverything/infra';
import type { TemplateResult } from 'lit'; import type { TemplateResult } from 'lit';
import { z } from 'zod'; import { z } from 'zod';
import {
KeyboardToolbarExtension,
mobileCodeConfig,
mobileParagraphConfig,
MobileSpecsPatches,
} from '../extensions/mobile-config';
const optionsSchema = z.object({ const optionsSchema = z.object({
// services // services
framework: z.instanceof(FrameworkProvider), framework: z.instanceof(FrameworkProvider),
@@ -106,7 +98,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
reactToLit, reactToLit,
confirmModal, confirmModal,
} = options; } = options;
const isMobileEdition = BUILD_CONFIG.isMobileEdition;
const isElectron = BUILD_CONFIG.isElectron; const isElectron = BUILD_CONFIG.isElectron;
const docService = framework.get(DocService); const docService = framework.get(DocService);
@@ -119,7 +110,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
patchReferenceRenderer(reactToLit, referenceRenderer), patchReferenceRenderer(reactToLit, referenceRenderer),
patchNotificationService(confirmModal), patchNotificationService(confirmModal),
patchOpenDocExtension(), patchOpenDocExtension(),
EdgelessClipboardAIChatConfig,
patchSideBarService(framework), patchSideBarService(framework),
patchDocModeService(docService, docsService, editorService), patchDocModeService(docService, docsService, editorService),
]); ]);
@@ -129,14 +119,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
patchDatabaseBlockConfigService(), patchDatabaseBlockConfigService(),
patchForAudioEmbedView(reactToLit), patchForAudioEmbedView(reactToLit),
]); ]);
if (isMobileEdition) {
context.register([
KeyboardToolbarExtension(framework),
MobileSpecsPatches,
mobileParagraphConfig,
mobileCodeConfig,
]);
}
if (isElectron) { if (isElectron) {
context.register(patchForClipboardInElectron(framework)); context.register(patchForClipboardInElectron(framework));
} }

View File

@@ -22,7 +22,9 @@ class MigratingAffineStoreExtension extends StoreExtensionProvider {
interface Configure { interface Configure {
init: () => Configure; init: () => Configure;
featureFlag: (featureFlagService?: FeatureFlagService) => Configure; featureFlag: (featureFlagService?: FeatureFlagService) => Configure;
value: StoreExtensionManager; value: StoreExtensionManager;
} }

View File

@@ -7,6 +7,7 @@ import {
import { AffineEditorConfigViewExtension } from '@affine/core/blocksuite/extensions/editor-config'; import { AffineEditorConfigViewExtension } from '@affine/core/blocksuite/extensions/editor-config';
import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database'; import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database';
import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked'; import { createLinkedWidgetConfig } from '@affine/core/blocksuite/extensions/editor-config/linked';
import { MobileViewExtension } from '@affine/core/blocksuite/extensions/mobile';
import { PdfViewExtension } from '@affine/core/blocksuite/extensions/pdf'; import { PdfViewExtension } from '@affine/core/blocksuite/extensions/pdf';
import { AffineThemeViewExtension } from '@affine/core/blocksuite/extensions/theme'; import { AffineThemeViewExtension } from '@affine/core/blocksuite/extensions/theme';
import { TurboRendererViewExtension } from '@affine/core/blocksuite/extensions/turbo-renderer'; import { TurboRendererViewExtension } from '@affine/core/blocksuite/extensions/turbo-renderer';
@@ -24,6 +25,25 @@ import type { FrameworkProvider } from '@toeverything/infra';
import { CodeBlockPreviewViewExtension } from './code-block-preview'; import { CodeBlockPreviewViewExtension } from './code-block-preview';
type Configure = {
init: () => Configure;
common: (framework?: FrameworkProvider, enableAI?: boolean) => Configure;
editorView: (options?: AffineEditorViewOptions) => Configure;
theme: (framework?: FrameworkProvider) => Configure;
editorConfig: (framework?: FrameworkProvider) => Configure;
edgelessBlockHeader: (options?: EdgelessBlockHeaderViewOptions) => Configure;
database: (framework?: FrameworkProvider) => Configure;
linkedDoc: (framework?: FrameworkProvider) => Configure;
paragraph: (enableAI?: boolean) => Configure;
cloud: (framework?: FrameworkProvider, enableCloud?: boolean) => Configure;
turboRenderer: (enableTurboRenderer?: boolean) => Configure;
pdf: (enablePDFEmbedPreview?: boolean, reactToLit?: ReactToLit) => Configure;
mobile: (framework?: FrameworkProvider) => Configure;
value: ViewExtensionManager;
};
class ViewProvider { class ViewProvider {
static instance: ViewProvider | null = null; static instance: ViewProvider | null = null;
static getInstance() { static getInstance() {
@@ -48,6 +68,7 @@ class ViewProvider {
TurboRendererViewExtension, TurboRendererViewExtension,
CloudViewExtension, CloudViewExtension,
PdfViewExtension, PdfViewExtension,
MobileViewExtension,
]); ]);
} }
@@ -55,7 +76,7 @@ class ViewProvider {
return this._manager; return this._manager;
} }
get config() { get config(): Configure {
return { return {
init: this._initDefaultConfig, init: this._initDefaultConfig,
common: this._configureCommon, common: this._configureCommon,
@@ -69,6 +90,7 @@ class ViewProvider {
cloud: this._configureCloud, cloud: this._configureCloud,
turboRenderer: this._configureTurboRenderer, turboRenderer: this._configureTurboRenderer,
pdf: this._configurePdf, pdf: this._configurePdf,
mobile: this._configureMobile,
value: this._manager, value: this._manager,
}; };
} }
@@ -85,7 +107,8 @@ class ViewProvider {
.paragraph() .paragraph()
.cloud() .cloud()
.turboRenderer() .turboRenderer()
.pdf(); .pdf()
.mobile();
return this.config; return this.config;
}; };
@@ -146,7 +169,23 @@ class ViewProvider {
}; };
private readonly _configureParagraph = (enableAI?: boolean) => { private readonly _configureParagraph = (enableAI?: boolean) => {
if (enableAI) { if (BUILD_CONFIG.isMobileEdition) {
this._manager.configure(ParagraphViewExtension, {
getPlaceholder: model => {
const placeholders = {
text: '',
h1: 'Heading 1',
h2: 'Heading 2',
h3: 'Heading 3',
h4: 'Heading 4',
h5: 'Heading 5',
h6: 'Heading 6',
quote: '',
};
return placeholders[model.props.type] ?? '';
},
});
} else if (enableAI) {
this._manager.configure(ParagraphViewExtension, { this._manager.configure(ParagraphViewExtension, {
getPlaceholder: model => { getPlaceholder: model => {
const placeholders = { const placeholders = {
@@ -193,6 +232,11 @@ class ViewProvider {
}); });
return this.config; return this.config;
}; };
private readonly _configureMobile = (framework?: FrameworkProvider) => {
this._manager.configure(MobileViewExtension, { framework });
return this.config;
};
} }
export function getViewManager() { export function getViewManager() {