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

@@ -98,7 +98,8 @@ const usePatchSpecs = (mode: DocMode) => {
})
.database(framework)
.linkedDoc(framework)
.paragraph(enableAI).value;
.paragraph(enableAI)
.mobile(framework).value;
if (BUILD_CONFIG.isMobileEdition) {
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 { 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';
import {
FeatureFlagService,
VirtualKeyboardProvider as BSVirtualKeyboardProvider,
type VirtualKeyboardProviderWithAction,
} 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 { batch, signal } from '@preact/signals-core';
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(
framework: FrameworkProvider
): ExtensionType {
@@ -89,6 +54,7 @@ export function KeyboardToolbarExtension(
if ('show' in affineVirtualKeyboardProvider) {
const providerWithAction = affineVirtualKeyboardProvider;
class BSVirtualKeyboardServiceWithShowAndHide
extends BSVirtualKeyboardService
implements VirtualKeyboardProviderWithAction
@@ -96,6 +62,7 @@ export function KeyboardToolbarExtension(
show() {
providerWithAction.show();
}
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 { edgelessCopilotWidget } from '@affine/core/blocksuite/ai/widgets/edgeless-copilot';
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 { getFontConfigExtension } from '@affine/core/blocksuite/extensions/font-config';
import { patchPeekViewService } from '@affine/core/blocksuite/extensions/peek-view-service';
@@ -41,15 +42,17 @@ export class AffineCommonViewExtension extends ViewExtensionProvider<
context: ViewExtensionContext,
framework: FrameworkProvider
) {
context.register(AIChatBlockSpec);
context.register(AITranscriptionBlockSpec);
context.register([
AICodeBlockWatcher,
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:image'),
config: imageToolbarAIEntryConfig(),
}),
]);
context
.register(AIChatBlockSpec)
.register(AITranscriptionBlockSpec)
.register(EdgelessClipboardAIChatConfig)
.register(AICodeBlockWatcher)
.register(
ToolbarModuleExtension({
id: BlockFlavourIdentifier('custom:affine:image'),
config: imageToolbarAIEntryConfig(),
})
);
if (context.scope === 'edgeless' || context.scope === 'page') {
context.register([
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 { patchDocModeService } from '@affine/core/blocksuite/extensions/doc-mode-service';
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 { patchNotificationService } from '@affine/core/blocksuite/extensions/notification-service';
import { patchOpenDocExtension } from '@affine/core/blocksuite/extensions/open-doc';
@@ -29,13 +28,6 @@ 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({
// services
framework: z.instanceof(FrameworkProvider),
@@ -106,7 +98,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
reactToLit,
confirmModal,
} = options;
const isMobileEdition = BUILD_CONFIG.isMobileEdition;
const isElectron = BUILD_CONFIG.isElectron;
const docService = framework.get(DocService);
@@ -119,7 +110,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
patchReferenceRenderer(reactToLit, referenceRenderer),
patchNotificationService(confirmModal),
patchOpenDocExtension(),
EdgelessClipboardAIChatConfig,
patchSideBarService(framework),
patchDocModeService(docService, docsService, editorService),
]);
@@ -129,14 +119,6 @@ export class AffineEditorViewExtension extends ViewExtensionProvider<AffineEdito
patchDatabaseBlockConfigService(),
patchForAudioEmbedView(reactToLit),
]);
if (isMobileEdition) {
context.register([
KeyboardToolbarExtension(framework),
MobileSpecsPatches,
mobileParagraphConfig,
mobileCodeConfig,
]);
}
if (isElectron) {
context.register(patchForClipboardInElectron(framework));
}

View File

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

View File

@@ -7,6 +7,7 @@ import {
import { AffineEditorConfigViewExtension } from '@affine/core/blocksuite/extensions/editor-config';
import { createDatabaseOptionsConfig } from '@affine/core/blocksuite/extensions/editor-config/database';
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 { AffineThemeViewExtension } from '@affine/core/blocksuite/extensions/theme';
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';
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 {
static instance: ViewProvider | null = null;
static getInstance() {
@@ -48,6 +68,7 @@ class ViewProvider {
TurboRendererViewExtension,
CloudViewExtension,
PdfViewExtension,
MobileViewExtension,
]);
}
@@ -55,7 +76,7 @@ class ViewProvider {
return this._manager;
}
get config() {
get config(): Configure {
return {
init: this._initDefaultConfig,
common: this._configureCommon,
@@ -69,6 +90,7 @@ class ViewProvider {
cloud: this._configureCloud,
turboRenderer: this._configureTurboRenderer,
pdf: this._configurePdf,
mobile: this._configureMobile,
value: this._manager,
};
}
@@ -85,7 +107,8 @@ class ViewProvider {
.paragraph()
.cloud()
.turboRenderer()
.pdf();
.pdf()
.mobile();
return this.config;
};
@@ -146,7 +169,23 @@ class ViewProvider {
};
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, {
getPlaceholder: model => {
const placeholders = {
@@ -193,6 +232,11 @@ class ViewProvider {
});
return this.config;
};
private readonly _configureMobile = (framework?: FrameworkProvider) => {
this._manager.configure(MobileViewExtension, { framework });
return this.config;
};
}
export function getViewManager() {