diff --git a/blocksuite/affine/block-root/src/common-specs/widgets.ts b/blocksuite/affine/block-root/src/common-specs/widgets.ts index ecb62074277bd..a247391d6711a 100644 --- a/blocksuite/affine/block-root/src/common-specs/widgets.ts +++ b/blocksuite/affine/block-root/src/common-specs/widgets.ts @@ -1,4 +1,3 @@ -import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring'; import { WidgetViewExtension } from '@blocksuite/block-std'; import { literal, unsafeStatic } from 'lit/static-html.js'; @@ -45,8 +44,3 @@ export const viewportOverlayWidget = WidgetViewExtension( AFFINE_VIEWPORT_OVERLAY_WIDGET, literal`${unsafeStatic(AFFINE_VIEWPORT_OVERLAY_WIDGET)}` ); -export const scrollAnchoringWidget = WidgetViewExtension( - 'affine:page', - AFFINE_SCROLL_ANCHORING_WIDGET, - literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}` -); diff --git a/packages/frontend/component/src/lit-react/index.ts b/packages/frontend/component/src/lit-react/index.ts index fb388ef511384..f4335f5bd55e3 100644 --- a/packages/frontend/component/src/lit-react/index.ts +++ b/packages/frontend/component/src/lit-react/index.ts @@ -1,4 +1,7 @@ -export { createComponent as createReactComponentFromLit } from './create-component'; +export { + createComponent as createReactComponentFromLit, + type ReactWebComponent, +} from './create-component'; export * from './lit-portal'; export { toReactNode } from './to-react-node'; export { templateToString } from './utils'; diff --git a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts index f5039e55e80fb..11b8ad25d3211 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/text-renderer.ts @@ -1,3 +1,4 @@ +import { createReactComponentFromLit } from '@affine/component'; import { BlockStdScope, BlockViewIdentifier, @@ -30,6 +31,7 @@ import { property, query } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { keyed } from 'lit/directives/keyed.js'; import { literal } from 'lit/static-html.js'; +import React from 'react'; import { markDownToDoc } from '../../utils'; import type { @@ -349,12 +351,6 @@ export class TextRenderer extends WithDisposable(ShadowlessElement) { accessor state: AffineAIPanelState | undefined = undefined; } -declare global { - interface HTMLElementTagNameMap { - 'text-renderer': TextRenderer; - } -} - export const createTextRenderer: ( host: EditorHost, options: TextRendererOptions @@ -368,3 +364,14 @@ export const createTextRenderer: ( >`; }; }; + +export const LitTextRenderer = createReactComponentFromLit({ + react: React, + elementClass: TextRenderer, +}); + +declare global { + interface HTMLElementTagNameMap { + 'text-renderer': TextRenderer; + } +} diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts b/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts index 5db82b9674149..2d07fa8097308 100644 --- a/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts +++ b/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts @@ -1,3 +1,4 @@ +import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { CodeBlockSpec, EdgelessRootBlockSpec, @@ -19,6 +20,12 @@ export function enableAIExtension( specBuilder: SpecBuilder, framework: FrameworkProvider ) { + const featureFlagService = framework.get(FeatureFlagService); + const enableAI = featureFlagService.flags.enable_ai.value; + if (!enableAI) { + return; + } + specBuilder.replace(CodeBlockSpec, AICodeBlockSpec); specBuilder.replace(ImageBlockSpec, AIImageBlockSpec); specBuilder.replace(ParagraphBlockSpec, AIParagraphBlockSpec); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx index 0752f7574e3b2..34d7f3fa78539 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx @@ -1,9 +1,4 @@ -import { - Button, - createReactComponentFromLit, - Divider, - useLitPortalFactory, -} from '@affine/component'; +import { Button, Divider, useLitPortalFactory } from '@affine/component'; import { DocService } from '@affine/core/modules/doc'; import { type Backlink, @@ -28,7 +23,7 @@ import { useLiveData, useServices, } from '@toeverything/infra'; -import React, { +import { Fragment, type ReactNode, useCallback, @@ -40,19 +35,14 @@ import { AffinePageReference, AffineSharedPageReference, } from '../../components/affine/reference-link'; -import { TextRenderer } from '../ai/components/text-renderer'; -import * as styles from './bi-directional-link-panel.css'; +import { LitTextRenderer } from '../ai/components/text-renderer'; import { patchReferenceRenderer, type ReferenceReactRenderer, -} from './specs/custom/spec-patchers'; +} from '../extensions/reference-renderer'; +import * as styles from './bi-directional-link-panel.css'; import { createPageModeSpecs } from './specs/page'; -const BlocksuiteTextRenderer = createReactComponentFromLit({ - react: React, - elementClass: TextRenderer, -}); - const PREFIX = 'bi-directional-link-panel-collapse:'; const useBiDirectionalLinkPanelCollapseState = ( @@ -288,7 +278,7 @@ export const BacklinkGroups = () => { /> ) : ( - { patchNotificationService(confirmModal), patchPeekViewService(peekViewService), patchOpenDocExtension(), - patchEdgelessClipboard(), - patchParseDocUrlExtension(framework), - patchGenerateDocUrlExtension(framework), + EdgelessClipboardWatcher, + patchDocUrlExtensions(framework), patchQuickSearchService(framework), patchSideBarService(framework), patchDocModeService(docService, docsService, editorService), @@ -178,13 +155,7 @@ const usePatchSpecs = (mode: DocMode) => { builder.extend([patchForAttachmentEmbedViews(reactToLit)]); } if (BUILD_CONFIG.isMobileEdition) { - builder.omit(formatBarWidget); - builder.omit(embedCardToolbarWidget); - builder.omit(slashMenuWidget); - builder.omit(codeToolbarWidget); - builder.omit(imageToolbarWidget); - builder.omit(surfaceRefToolbarWidget); - builder.extend([patchForMobile()].flat()); + enableMobileExtension(builder); } if (BUILD_CONFIG.isElectron) { builder.extend([patchForClipboardInElectron(framework)].flat()); @@ -319,7 +290,7 @@ export const BlocksuiteDocEditor = forwardRef< <>
{!isJournal ? ( - + ) : ( )} @@ -335,7 +306,7 @@ export const BlocksuiteDocEditor = forwardRef< />
) : null} - - + {portals} ); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts deleted file mode 100644 index 97f31275a1dce..0000000000000 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { DocService, DocsService } from '@affine/core/modules/doc'; -import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; -import { EditorSettingService } from '@affine/core/modules/editor-setting'; -import { AppThemeService } from '@affine/core/modules/theme'; -import { mixpanel } from '@affine/track'; -import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std'; -import type { - DocDisplayMetaExtension, - DocDisplayMetaParams, - Signal, - SpecBuilder, - TelemetryEventMap, - ThemeExtension, -} from '@blocksuite/affine/blocks'; -import { - ColorScheme, - createSignalFromObservable, - DatabaseConfigExtension, - DocDisplayMetaProvider, - EditorSettingExtension, - referenceToNode, - RootBlockConfigExtension, - SpecProvider, - TelemetryProvider, - ThemeExtensionIdentifier, - ToolbarMoreMenuConfigExtension, -} from '@blocksuite/affine/blocks'; -import type { Container } from '@blocksuite/affine/global/di'; -import type { ExtensionType } from '@blocksuite/affine/store'; -import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit'; -import { type FrameworkProvider } from '@toeverything/infra'; -import type { TemplateResult } from 'lit'; -import type { Observable } from 'rxjs'; -import { combineLatest, map } from 'rxjs'; - -import { getFontConfigExtension } from '../font-extension'; -import { createDatabaseOptionsConfig } from './database-block'; -import { createLinkedWidgetConfig } from './widgets/linked'; -import { createToolbarMoreMenuConfig } from './widgets/toolbar'; - -function getTelemetryExtension(): ExtensionType { - return { - setup: di => { - di.addImpl(TelemetryProvider, () => ({ - track: ( - eventName: T, - props: TelemetryEventMap[T] - ) => { - mixpanel.track(eventName as string, props as Record); - }, - })); - }, - }; -} - -function getThemeExtension(framework: FrameworkProvider) { - class AffineThemeExtension - extends LifeCycleWatcher - implements ThemeExtension - { - static override readonly key = 'affine-theme'; - - private readonly themes: Map> = new Map(); - - protected readonly disposables: (() => void)[] = []; - - static override setup(di: Container) { - super.setup(di); - di.override(ThemeExtensionIdentifier, AffineThemeExtension, [ - StdIdentifier, - ]); - } - - getAppTheme() { - const keyName = 'app-theme'; - const cache = this.themes.get(keyName); - if (cache) return cache; - - const theme$: Observable = framework - .get(AppThemeService) - .appTheme.theme$.map(theme => { - return theme === ColorScheme.Dark - ? ColorScheme.Dark - : ColorScheme.Light; - }); - const { signal: themeSignal, cleanup } = - createSignalFromObservable(theme$, ColorScheme.Light); - this.disposables.push(cleanup); - this.themes.set(keyName, themeSignal); - return themeSignal; - } - - getEdgelessTheme(docId?: string) { - const doc = - (docId && framework.get(DocsService).list.doc$(docId).getValue()) || - framework.get(DocService).doc; - - const cache = this.themes.get(doc.id); - if (cache) return cache; - - const appTheme$ = framework.get(AppThemeService).appTheme.theme$; - const docTheme$ = doc.properties$.map( - props => props.edgelessColorTheme || 'system' - ); - const theme$: Observable = combineLatest([ - appTheme$, - docTheme$, - ]).pipe( - map(([appTheme, docTheme]) => { - const theme = docTheme === 'system' ? appTheme : docTheme; - return theme === ColorScheme.Dark - ? ColorScheme.Dark - : ColorScheme.Light; - }) - ); - const { signal: themeSignal, cleanup } = - createSignalFromObservable(theme$, ColorScheme.Light); - this.disposables.push(cleanup); - this.themes.set(doc.id, themeSignal); - return themeSignal; - } - - override unmounted() { - this.dispose(); - } - - dispose() { - this.disposables.forEach(dispose => dispose()); - } - } - - return AffineThemeExtension; -} - -export function buildDocDisplayMetaExtension(framework: FrameworkProvider) { - const docDisplayMetaService = framework.get(DocDisplayMetaService); - - function iconBuilder( - icon: typeof PageIcon, - size = '1.25em', - style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;' - ) { - return icon({ - width: size, - height: size, - style, - }); - } - - class AffineDocDisplayMetaService - extends LifeCycleWatcher - implements DocDisplayMetaExtension - { - static override key = 'doc-display-meta'; - - readonly disposables: (() => void)[] = []; - - static override setup(di: Container) { - super.setup(di); - di.override(DocDisplayMetaProvider, this, [StdIdentifier]); - } - - dispose() { - while (this.disposables.length > 0) { - this.disposables.pop()?.(); - } - } - - icon( - docId: string, - { params, title, referenced }: DocDisplayMetaParams = {} - ): Signal { - const icon$ = docDisplayMetaService - .icon$(docId, { - type: 'lit', - title, - reference: referenced, - referenceToNode: referenceToNode({ pageId: docId, params }), - }) - .map(iconBuilder); - - const { signal: iconSignal, cleanup } = createSignalFromObservable( - icon$, - iconBuilder(referenced ? LinkedPageIcon : PageIcon) - ); - - this.disposables.push(cleanup); - - return iconSignal; - } - - title( - docId: string, - { title, referenced }: DocDisplayMetaParams = {} - ): Signal { - const title$ = docDisplayMetaService.title$(docId, { - title, - reference: referenced, - }); - - const { signal: titleSignal, cleanup } = - createSignalFromObservable(title$, title ?? ''); - - this.disposables.push(cleanup); - - return titleSignal; - } - - override unmounted() { - this.dispose(); - } - } - - return AffineDocDisplayMetaService; -} - -function getEditorConfigExtension( - framework: FrameworkProvider -): ExtensionType[] { - const editorSettingService = framework.get(EditorSettingService); - return [ - EditorSettingExtension(editorSettingService.editorSetting.settingSignal), - DatabaseConfigExtension(createDatabaseOptionsConfig(framework)), - RootBlockConfigExtension({ - linkedWidget: createLinkedWidgetConfig(framework), - }), - ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)), - ]; -} - -export const extendEdgelessPreviewSpec = (function () { - let _extension: ExtensionType; - let _framework: FrameworkProvider; - return function (framework: FrameworkProvider) { - if (framework === _framework && _extension) { - return _extension; - } else { - _extension && SpecProvider._.omitSpec('preview:edgeless', _extension); - _extension = getThemeExtension(framework); - _framework = framework; - SpecProvider._.extendSpec('preview:edgeless', [_extension]); - return _extension; - } - }; -})(); - -export function enableAffineExtension( - framework: FrameworkProvider, - specBuilder: SpecBuilder -): void { - specBuilder.extend( - [ - getThemeExtension(framework), - getFontConfigExtension(), - getTelemetryExtension(), - getEditorConfigExtension(framework), - buildDocDisplayMetaExtension(framework), - ].flat() - ); -} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx deleted file mode 100644 index 3481d93e4fd8c..0000000000000 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/spec-patchers.tsx +++ /dev/null @@ -1,761 +0,0 @@ -import { - type ElementOrFactory, - Input, - notify, - toast, - type ToastOptions, - toReactNode, - type useConfirmModal, -} from '@affine/component'; -import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks'; -import { WorkspaceServerService } from '@affine/core/modules/cloud'; -import { DesktopApiService } from '@affine/core/modules/desktop-api'; -import { type DocService, DocsService } from '@affine/core/modules/doc'; -import type { EditorService } from '@affine/core/modules/editor'; -import { EditorSettingService } from '@affine/core/modules/editor-setting'; -import { JournalService } from '@affine/core/modules/journal'; -import { resolveLinkToDoc } from '@affine/core/modules/navigation'; -import type { PeekViewService } from '@affine/core/modules/peek-view'; -import { - CreationQuickSearchSession, - DocsQuickSearchSession, - LinksQuickSearchSession, - QuickSearchService, - RecentDocsQuickSearchSession, -} from '@affine/core/modules/quicksearch'; -import { ExternalLinksQuickSearchSession } from '@affine/core/modules/quicksearch/impls/external-links'; -import { JournalsQuickSearchSession } from '@affine/core/modules/quicksearch/impls/journals'; -import { WorkbenchService } from '@affine/core/modules/workbench'; -import { WorkspaceService } from '@affine/core/modules/workspace'; -import { DebugLogger } from '@affine/debug'; -import { I18n } from '@affine/i18n'; -import { track } from '@affine/track'; -import { - BlockServiceWatcher, - type BlockStdScope, - BlockViewIdentifier, - ConfigIdentifier, - LifeCycleWatcher, - type WidgetComponent, -} from '@blocksuite/affine/block-std'; -import type { - AffineReference, - CodeBlockConfig, - DocMode, - DocModeProvider, - OpenDocConfig, - OpenDocConfigItem, - PeekOptions, - PeekViewService as BSPeekViewService, - QuickSearchResult, - ReferenceNodeConfig, - RootBlockConfig, -} from '@blocksuite/affine/blocks'; -import { - AffineSlashMenuWidget, - AttachmentEmbedConfigIdentifier, - DocModeExtension, - EdgelessRootBlockComponent, - EmbedLinkedDocBlockComponent, - FeatureFlagService, - GenerateDocUrlExtension, - insertLinkByQuickSearchCommand, - NativeClipboardExtension, - NoteConfigExtension, - NotificationExtension, - OpenDocExtension, - ParagraphBlockService, - ParseDocUrlExtension, - PeekViewExtension, - QuickSearchExtension, - ReferenceNodeConfigExtension, - ReferenceNodeConfigIdentifier, - RootBlockConfigExtension, - SidebarExtension, -} from '@blocksuite/affine/blocks'; -import type { Container } from '@blocksuite/affine/global/di'; -import { Bound } from '@blocksuite/affine/global/utils'; -import { - type BlockSnapshot, - type ExtensionType, - Text, -} from '@blocksuite/affine/store'; -import type { ReferenceParams } from '@blocksuite/affine-model'; -import { - CenterPeekIcon, - ExpandFullIcon, - OpenInNewIcon, - SplitViewIcon, -} from '@blocksuite/icons/lit'; -import { type FrameworkProvider } from '@toeverything/infra'; -import { html, type TemplateResult } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import { literal } from 'lit/static-html.js'; -import { pick } from 'lodash-es'; - -import { AttachmentEmbedPreview } from '../../../../components/attachment-viewer/pdf-viewer-embedded'; -import { generateUrl } from '../../../../components/hooks/affine/use-share-url'; -import type { DocProps } from '../../../initialization'; -import { BlocksuiteEditorJournalDocTitle } from '../../journal-doc-title'; -import { EdgelessNoteHeader } from './widgets/edgeless-note-header'; -import { createKeyboardToolbarConfig } from './widgets/keyboard-toolbar'; - -export type ReferenceReactRenderer = ( - reference: AffineReference -) => React.ReactElement; - -const logger = new DebugLogger('affine::spec-patchers'); - -function patchSpecService( - flavour: string, - onWidgetConnected?: (component: WidgetComponent) => void -) { - class TempServiceWatcher extends BlockServiceWatcher { - static override readonly flavour = flavour; - override mounted() { - super.mounted(); - const disposableGroup = this.blockService.disposables; - if (onWidgetConnected) { - disposableGroup.add( - this.blockService.specSlots.widgetConnected.on(({ component }) => { - onWidgetConnected(component); - }) - ); - } - } - } - return TempServiceWatcher; -} - -/** - * Patch the block specs with custom renderers. - */ -export function patchReferenceRenderer( - reactToLit: (element: ElementOrFactory) => TemplateResult, - reactRenderer: ReferenceReactRenderer -): ExtensionType { - const customContent = (reference: AffineReference) => { - const node = reactRenderer(reference); - return reactToLit(node); - }; - - return ReferenceNodeConfigExtension({ - customContent, - }); -} - -export function patchNotificationService({ - closeConfirmModal, - openConfirmModal, -}: ReturnType) { - return NotificationExtension({ - confirm: async ({ title, message, confirmText, cancelText, abort }) => { - return new Promise(resolve => { - openConfirmModal({ - title: toReactNode(title), - description: toReactNode(message), - confirmText, - confirmButtonOptions: { - variant: 'primary', - }, - cancelText, - onConfirm: () => { - resolve(true); - }, - onCancel: () => { - resolve(false); - }, - }); - abort?.addEventListener('abort', () => { - resolve(false); - closeConfirmModal(); - }); - }); - }, - prompt: async ({ - title, - message, - confirmText, - placeholder, - cancelText, - autofill, - abort, - }) => { - return new Promise(resolve => { - let value = autofill || ''; - const description = ( -
- {toReactNode(message)} - (value = e)} - /> -
- ); - openConfirmModal({ - title: toReactNode(title), - description: description, - confirmText: confirmText ?? 'Confirm', - confirmButtonOptions: { - variant: 'primary', - }, - cancelText: cancelText ?? 'Cancel', - onConfirm: () => { - resolve(value); - }, - onCancel: () => { - resolve(null); - }, - autoFocusConfirm: false, - }); - abort?.addEventListener('abort', () => { - resolve(null); - closeConfirmModal(); - }); - }); - }, - toast: (message: string, options: ToastOptions) => { - return toast(message, options); - }, - notify: notification => { - const accentToNotify = { - error: notify.error, - success: notify.success, - warning: notify.warning, - info: notify, - }; - - const fn = accentToNotify[notification.accent || 'info']; - if (!fn) { - throw new Error('Invalid notification accent'); - } - - const toastId = fn( - { - title: toReactNode(notification.title), - message: toReactNode(notification.message), - footer: toReactNode(notification.footer), - action: notification.action?.onClick - ? { - label: toReactNode(notification.action?.label), - onClick: notification.action.onClick, - } - : undefined, - onDismiss: notification.onClose, - }, - { - duration: notification.duration || 0, - onDismiss: notification.onClose, - onAutoClose: notification.onClose, - } - ); - - notification.abort?.addEventListener('abort', () => { - notify.dismiss(toastId); - }); - }, - }); -} - -export function patchOpenDocExtension() { - const openDocConfig: OpenDocConfig = { - items: [ - { - type: 'open-in-active-view', - label: I18n['com.affine.peek-view-controls.open-doc'](), - icon: ExpandFullIcon(), - }, - BUILD_CONFIG.isElectron - ? { - type: 'open-in-new-view', - label: - I18n['com.affine.peek-view-controls.open-doc-in-split-view'](), - icon: SplitViewIcon(), - } - : null, - { - type: 'open-in-new-tab', - label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](), - icon: OpenInNewIcon(), - }, - { - type: 'open-in-center-peek', - label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](), - icon: CenterPeekIcon(), - }, - ].filter((item): item is OpenDocConfigItem => item !== null), - }; - return OpenDocExtension(openDocConfig); -} - -export function patchPeekViewService(service: PeekViewService) { - return PeekViewExtension({ - peek: ( - element: { - target: HTMLElement; - docId: string; - blockIds?: string[]; - template?: TemplateResult; - }, - options?: PeekOptions - ) => { - logger.debug('center peek', element); - const { template, target, ...props } = element; - - return service.peekView.open( - { - element: target, - docRef: props, - }, - template, - options?.abortSignal - ); - }, - } satisfies BSPeekViewService); -} - -export function patchDocModeService( - docService: DocService, - docsService: DocsService, - editorService: EditorService -): ExtensionType { - const DEFAULT_MODE = 'page'; - class AffineDocModeService implements DocModeProvider { - setEditorMode = (mode: DocMode) => { - editorService.editor.setMode(mode); - }; - getEditorMode = () => { - return editorService.editor.mode$.value; - }; - setPrimaryMode = (mode: DocMode, id?: string) => { - if (id) { - docsService.list.setPrimaryMode(id, mode); - } else { - docService.doc.setPrimaryMode(mode); - } - }; - getPrimaryMode = (id?: string) => { - const mode = id - ? docsService.list.getPrimaryMode(id) - : docService.doc.getPrimaryMode(); - return (mode || DEFAULT_MODE) as DocMode; - }; - togglePrimaryMode = (id?: string) => { - const mode = id - ? docsService.list.togglePrimaryMode(id) - : docService.doc.togglePrimaryMode(); - return (mode || DEFAULT_MODE) as DocMode; - }; - onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => { - const mode$ = id - ? docsService.list.primaryMode$(id) - : docService.doc.primaryMode$; - const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode)); - return { - dispose: sub.unsubscribe, - }; - }; - } - - const docModeExtension = DocModeExtension(new AffineDocModeService()); - - return docModeExtension; -} - -export function patchQuickSearchService(framework: FrameworkProvider) { - const QuickSearch = QuickSearchExtension({ - async openQuickSearch() { - let searchResult: QuickSearchResult = null; - searchResult = await new Promise((resolve, reject) => - framework.get(QuickSearchService).quickSearch.show( - [ - framework.get(RecentDocsQuickSearchSession), - framework.get(CreationQuickSearchSession), - framework.get(DocsQuickSearchSession), - framework.get(LinksQuickSearchSession), - framework.get(ExternalLinksQuickSearchSession), - framework.get(JournalsQuickSearchSession), - ], - result => { - if (result === null) { - resolve(null); - return; - } - - if (result.source === 'docs') { - resolve({ - docId: result.payload.docId, - }); - return; - } - - if (result.source === 'recent-doc') { - resolve({ - docId: result.payload.docId, - }); - return; - } - - if (result.source === 'link') { - resolve({ - docId: result.payload.docId, - params: pick(result.payload, [ - 'mode', - 'blockIds', - 'elementIds', - ]), - }); - return; - } - - if (result.source === 'date-picker') { - result.payload - .getDocId() - .then(docId => { - if (docId) { - resolve({ docId }); - } - }) - .catch(reject); - return; - } - - if (result.source === 'external-link') { - const externalUrl = result.payload.url; - resolve({ externalUrl }); - return; - } - - if (result.source === 'creation') { - const docsService = framework.get(DocsService); - const editorSettingService = framework.get(EditorSettingService); - const mode = - result.id === 'creation:create-edgeless' ? 'edgeless' : 'page'; - const docProps: DocProps = { - page: { title: new Text(result.payload.title) }, - note: editorSettingService.editorSetting.get('affine:note'), - }; - const newDoc = docsService.createDoc({ - primaryMode: mode, - docProps, - }); - - resolve({ docId: newDoc.id }); - return; - } - }, - { - label: { - i18nKey: 'com.affine.cmdk.insert-links', - }, - placeholder: { - i18nKey: 'com.affine.cmdk.docs.placeholder', - }, - } - ) - ); - - return searchResult; - }, - }); - - const SlashMenuQuickSearchExtension = patchSpecService( - 'affine:page', - (component: WidgetComponent) => { - if (component instanceof AffineSlashMenuWidget) { - component.config.items.forEach(item => { - if ( - 'action' in item && - (item.name === 'Linked Doc' || item.name === 'Link') - ) { - item.action = async ({ rootComponent }) => { - const [success, { insertedLinkType }] = - rootComponent.std.command.exec(insertLinkByQuickSearchCommand); - - if (!success) return; - - insertedLinkType - ?.then(type => { - const flavour = type?.flavour; - if (!flavour) return; - - if (flavour === 'affine:bookmark') { - track.doc.editor.slashMenu.bookmark(); - return; - } - - if (flavour === 'affine:embed-linked-doc') { - track.doc.editor.slashMenu.linkDoc({ - control: 'linkDoc', - }); - return; - } - }) - .catch(console.error); - }; - } - }); - } - } - ); - return [QuickSearch, SlashMenuQuickSearchExtension]; -} - -export function patchParseDocUrlExtension(framework: FrameworkProvider) { - const workspaceService = framework.get(WorkspaceService); - const ParseDocUrl = ParseDocUrlExtension({ - parseDocUrl(url) { - const info = resolveLinkToDoc(url); - if (!info || info.workspaceId !== workspaceService.workspace.id) return; - - delete info.refreshKey; - - return info; - }, - }); - - return [ParseDocUrl]; -} - -export function patchGenerateDocUrlExtension(framework: FrameworkProvider) { - const workspaceService = framework.get(WorkspaceService); - const workspaceServerService = framework.get(WorkspaceServerService); - const GenerateDocUrl = GenerateDocUrlExtension({ - generateDocUrl(pageId: string, params?: ReferenceParams) { - return generateUrl({ - ...params, - pageId, - workspaceId: workspaceService.workspace.id, - baseUrl: workspaceServerService.server?.baseUrl ?? location.origin, - }); - }, - }); - - return [GenerateDocUrl]; -} - -export function patchEdgelessClipboard() { - class EdgelessClipboardWatcher extends BlockServiceWatcher { - static override readonly flavour = 'affine:page'; - - override mounted() { - super.mounted(); - this.blockService.disposables.add( - this.blockService.specSlots.viewConnected.on(view => { - const { component } = view; - if (component instanceof EdgelessRootBlockComponent) { - const AIChatBlockFlavour = AIChatBlockSchema.model.flavour; - const createFunc = (block: BlockSnapshot) => { - const { - xywh, - scale, - messages, - sessionId, - rootDocId, - rootWorkspaceId, - } = block.props; - const blockId = component.service.crud.addBlock( - AIChatBlockFlavour, - { - xywh, - scale, - messages, - sessionId, - rootDocId, - rootWorkspaceId, - }, - component.surface.model.id - ); - return blockId; - }; - component.clipboardController.registerBlock( - AIChatBlockFlavour, - createFunc - ); - } - }) - ); - } - } - - return EdgelessClipboardWatcher; -} - -@customElement('affine-linked-doc-ref-block') -export class LinkedDocBlockComponent extends EmbedLinkedDocBlockComponent { - override getInitialState() { - return { - loading: false, - isBannerEmpty: true, - }; - } -} - -export function patchForSharedPage() { - const extension: ExtensionType = { - setup: di => { - di.override( - BlockViewIdentifier('affine:embed-linked-doc'), - () => literal`affine-linked-doc-ref-block` - ); - di.override( - BlockViewIdentifier('affine:embed-synced-doc'), - () => literal`affine-linked-doc-ref-block` - ); - }, - }; - return extension; -} - -export function patchForMobile() { - 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); - } - - static override setup(di: Container) { - super.setup(di); - - // Hide reference popup on mobile. - { - const prev = di.getFactory(ReferenceNodeConfigIdentifier); - di.override(ReferenceNodeConfigIdentifier, provider => { - return { - ...prev?.(provider), - hidePopup: true, - } satisfies ReferenceNodeConfig; - }); - } - - // Hide number lines for code block on mobile. - { - const codeConfigIdentifier = ConfigIdentifier('affine:code'); - const prev = di.getFactory(codeConfigIdentifier); - di.override(codeConfigIdentifier, provider => { - return { - ...prev?.(provider), - showLineNumbers: false, - } satisfies CodeBlockConfig; - }); - } - } - - override mounted() { - // remove slash placeholder for mobile: `type / ...` - { - const paragraphService = this.std.get(ParagraphBlockService); - if (!paragraphService) return; - - paragraphService.placeholderGenerator = 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.type]; - }; - } - } - } - const extensions: ExtensionType[] = [ - { - setup: di => { - const prev = di.getFactory(RootBlockConfigExtension.identifier); - - di.override(RootBlockConfigExtension.identifier, provider => { - return { - ...prev?.(provider), - keyboardToolbar: createKeyboardToolbarConfig(), - } satisfies RootBlockConfig; - }); - }, - }, - MobileSpecsPatches, - ]; - return extensions; -} - -export function patchForAttachmentEmbedViews( - reactToLit: ( - element: ElementOrFactory, - rerendering?: boolean - ) => TemplateResult -): ExtensionType { - return { - setup: di => { - di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({ - name: 'pdf', - check: (model, maxFileSize) => - model.type === 'application/pdf' && model.size <= maxFileSize, - action: model => { - const bound = Bound.deserialize(model.xywh); - bound.w = 537 + 24 + 2; - bound.h = 759 + 46 + 24 + 2; - model.doc.updateBlock(model, { - embed: true, - style: 'pdf', - xywh: bound.serialize(), - }); - }, - template: (model, _blobUrl) => - reactToLit(, false), - })); - }, - }; -} - -export function patchForClipboardInElectron(framework: FrameworkProvider) { - const desktopApi = framework.get(DesktopApiService); - return NativeClipboardExtension({ - copyAsPNG: desktopApi.handler.clipboard.copyAsPNG, - }); -} - -export function patchForEdgelessNoteConfig( - framework: FrameworkProvider, - reactToLit: (element: ElementOrFactory) => TemplateResult -) { - return NoteConfigExtension({ - edgelessNoteHeader: ({ note }) => - reactToLit(), - pageBlockTitle: ({ note }) => { - const journalService = framework.get(JournalService); - const isJournal = !!journalService.journalDate$(note.doc.id).value; - if (isJournal) { - return reactToLit(); - } else { - return html``; - } - }, - }); -} - -export function patchSideBarService(framework: FrameworkProvider) { - const { workbench } = framework.get(WorkbenchService); - - return SidebarExtension({ - open: (tabId?: string) => { - workbench.openSidebar(); - workbench.activeView$.value.activeSidebarTab(tabId ?? null); - }, - close: () => { - workbench.closeSidebar(); - }, - getTabIds: () => { - return workbench.activeView$.value.sidebarTabs$.value.map(tab => tab.id); - }, - }); -} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts index 50f2ade690543..a26f14f093fe0 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts @@ -1,23 +1,17 @@ import { enableAIExtension } from '@affine/core/blocksuite/ai'; -import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { enableAffineExtension } from '@affine/core/blocksuite/extensions'; import { builtInTemplates as builtInEdgelessTemplates } from '@affine/templates/edgeless'; import { builtInTemplates as builtInStickersTemplates } from '@affine/templates/stickers'; import type { SpecBuilder, TemplateManager } from '@blocksuite/affine/blocks'; import { EdgelessTemplatePanel, SpecProvider } from '@blocksuite/affine/blocks'; import { type FrameworkProvider } from '@toeverything/infra'; -import { enableAffineExtension } from './custom/root-block'; - export function createEdgelessModeSpecs( framework: FrameworkProvider ): SpecBuilder { - const featureFlagService = framework.get(FeatureFlagService); - const enableAI = featureFlagService.flags.enable_ai.value; const edgelessSpec = SpecProvider._.getSpec('edgeless'); - enableAffineExtension(framework, edgelessSpec); - if (enableAI) { - enableAIExtension(edgelessSpec, framework); - } + enableAffineExtension(edgelessSpec, framework); + enableAIExtension(edgelessSpec, framework); return edgelessSpec; } diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts index ea5ee6fecfca6..f074e016f0072 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts @@ -1,18 +1,11 @@ import { enableAIExtension } from '@affine/core/blocksuite/ai'; -import { FeatureFlagService } from '@affine/core/modules/feature-flag'; +import { enableAffineExtension } from '@affine/core/blocksuite/extensions'; import { type SpecBuilder, SpecProvider } from '@blocksuite/affine/blocks'; import { type FrameworkProvider } from '@toeverything/infra'; -import { enableAffineExtension } from './custom/root-block'; - export function createPageModeSpecs(framework: FrameworkProvider): SpecBuilder { - const featureFlagService = framework.get(FeatureFlagService); - const enableAI = featureFlagService.flags.enable_ai.value; - const provider = SpecProvider._; - const pageSpec = provider.getSpec('page'); - enableAffineExtension(framework, pageSpec); - if (enableAI) { - enableAIExtension(pageSpec, framework); - } + const pageSpec = SpecProvider._.getSpec('page'); + enableAffineExtension(pageSpec, framework); + enableAIExtension(pageSpec, framework); return pageSpec; } diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/preview.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/preview.ts index ef9a5dbccb493..9c728ec6c944b 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/preview.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/preview.ts @@ -20,9 +20,10 @@ import type { ExtensionType } from '@blocksuite/affine/store'; import type { FrameworkProvider } from '@toeverything/infra'; import type { Observable } from 'rxjs'; -import { buildDocDisplayMetaExtension } from './custom/root-block'; -import { patchPeekViewService } from './custom/spec-patchers'; -import { getFontConfigExtension } from './font-extension'; +import { buildDocDisplayMetaExtension } from '../../extensions/display-meta'; +import { getFontConfigExtension } from '../../extensions/font-config'; +import { patchPeekViewService } from '../../extensions/peek-view-service'; +import { getThemeExtension } from '../../extensions/theme'; const CustomSpecs: ExtensionType[] = [ AIChatBlockSpec, @@ -111,3 +112,19 @@ export function createPageModePreviewSpecs( ]); return pagePreviewSpec; } + +export const extendEdgelessPreviewSpec = (function () { + let _extension: ExtensionType; + let _framework: FrameworkProvider; + return function (framework: FrameworkProvider) { + if (framework === _framework && _extension) { + return _extension; + } else { + _extension && SpecProvider._.omitSpec('preview:edgeless', _extension); + _extension = getThemeExtension(framework); + _framework = framework; + SpecProvider._.extendSpec('preview:edgeless', [_extension]); + return _extension; + } + }; +})(); diff --git a/packages/frontend/core/src/blocksuite/editors/index.ts b/packages/frontend/core/src/blocksuite/editors/index.ts index 4ba445ba89ac4..4716eef1fe4e8 100644 --- a/packages/frontend/core/src/blocksuite/editors/index.ts +++ b/packages/frontend/core/src/blocksuite/editors/index.ts @@ -1,9 +1,28 @@ +import { createReactComponentFromLit } from '@affine/component'; +import { DocTitle } from '@blocksuite/affine/blocks'; +import React from 'react'; + import { EdgelessEditor } from './edgeless-editor'; import { PageEditor } from './page-editor'; export * from './edgeless-editor'; export * from './page-editor'; +export const LitDocEditor = createReactComponentFromLit({ + react: React, + elementClass: PageEditor, +}); + +export const LitDocTitle = createReactComponentFromLit({ + react: React, + elementClass: DocTitle, +}); + +export const LitEdgelessEditor = createReactComponentFromLit({ + react: React, + elementClass: EdgelessEditor, +}); + export function effects() { customElements.define('page-editor', PageEditor); customElements.define('edgeless-editor', EdgelessEditor); diff --git a/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx b/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx new file mode 100644 index 0000000000000..baff8bf8b1bb6 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/attachment-embed-view.tsx @@ -0,0 +1,35 @@ +import type { ElementOrFactory } from '@affine/component'; +import { AttachmentEmbedPreview } from '@affine/core/components/attachment-viewer/pdf-viewer-embedded'; +import { AttachmentEmbedConfigIdentifier } from '@blocksuite/affine/blocks'; +import { Bound } from '@blocksuite/affine/global/utils'; +import type { ExtensionType } from '@blocksuite/affine/store'; +import type { TemplateResult } from 'lit'; + +export function patchForAttachmentEmbedViews( + reactToLit: ( + element: ElementOrFactory, + rerendering?: boolean + ) => TemplateResult +): ExtensionType { + return { + setup: di => { + di.override(AttachmentEmbedConfigIdentifier('pdf'), () => ({ + name: 'pdf', + check: (model, maxFileSize) => + model.type === 'application/pdf' && model.size <= maxFileSize, + action: model => { + const bound = Bound.deserialize(model.xywh); + bound.w = 537 + 24 + 2; + bound.h = 759 + 46 + 24 + 2; + model.doc.updateBlock(model, { + embed: true, + style: 'pdf', + xywh: bound.serialize(), + }); + }, + template: (model, _blobUrl) => + reactToLit(, false), + })); + }, + }; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/display-meta.ts b/packages/frontend/core/src/blocksuite/extensions/display-meta.ts new file mode 100644 index 0000000000000..6fe888bd5abd2 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/display-meta.ts @@ -0,0 +1,98 @@ +import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; +import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std'; +import type { + DocDisplayMetaExtension, + DocDisplayMetaParams, + Signal, +} from '@blocksuite/affine/blocks'; +import { + createSignalFromObservable, + DocDisplayMetaProvider, + referenceToNode, +} from '@blocksuite/affine/blocks'; +import type { Container } from '@blocksuite/affine/global/di'; +import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit'; +import { type FrameworkProvider } from '@toeverything/infra'; +import type { TemplateResult } from 'lit'; + +export function buildDocDisplayMetaExtension(framework: FrameworkProvider) { + const docDisplayMetaService = framework.get(DocDisplayMetaService); + + function iconBuilder( + icon: typeof PageIcon, + size = '1.25em', + style = 'user-select:none;flex-shrink:0;vertical-align:middle;font-size:inherit;margin-bottom:0.1em;' + ) { + return icon({ + width: size, + height: size, + style, + }); + } + + class AffineDocDisplayMetaService + extends LifeCycleWatcher + implements DocDisplayMetaExtension + { + static override key = 'doc-display-meta'; + + readonly disposables: (() => void)[] = []; + + static override setup(di: Container) { + super.setup(di); + di.override(DocDisplayMetaProvider, this, [StdIdentifier]); + } + + dispose() { + while (this.disposables.length > 0) { + this.disposables.pop()?.(); + } + } + + icon( + docId: string, + { params, title, referenced }: DocDisplayMetaParams = {} + ): Signal { + const icon$ = docDisplayMetaService + .icon$(docId, { + type: 'lit', + title, + reference: referenced, + referenceToNode: referenceToNode({ pageId: docId, params }), + }) + .map(iconBuilder); + + const { signal: iconSignal, cleanup } = createSignalFromObservable( + icon$, + iconBuilder(referenced ? LinkedPageIcon : PageIcon) + ); + + this.disposables.push(cleanup); + + return iconSignal; + } + + title( + docId: string, + { title, referenced }: DocDisplayMetaParams = {} + ): Signal { + const title$ = docDisplayMetaService.title$(docId, { + title, + reference: referenced, + }); + + const { signal: titleSignal, cleanup } = + createSignalFromObservable(title$, title ?? ''); + + this.disposables.push(cleanup); + + return titleSignal; + } + + override unmounted() { + this.dispose(); + } + } + + return AffineDocDisplayMetaService; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/doc-mode-service.ts b/packages/frontend/core/src/blocksuite/extensions/doc-mode-service.ts new file mode 100644 index 0000000000000..0fbafaaa1827a --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/doc-mode-service.ts @@ -0,0 +1,53 @@ +import type { DocService, DocsService } from '@affine/core/modules/doc'; +import type { EditorService } from '@affine/core/modules/editor'; +import type { DocMode, DocModeProvider } from '@blocksuite/affine/blocks'; +import { DocModeExtension } from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; + +export function patchDocModeService( + docService: DocService, + docsService: DocsService, + editorService: EditorService +): ExtensionType { + const DEFAULT_MODE = 'page'; + class AffineDocModeService implements DocModeProvider { + setEditorMode = (mode: DocMode) => { + editorService.editor.setMode(mode); + }; + getEditorMode = () => { + return editorService.editor.mode$.value; + }; + setPrimaryMode = (mode: DocMode, id?: string) => { + if (id) { + docsService.list.setPrimaryMode(id, mode); + } else { + docService.doc.setPrimaryMode(mode); + } + }; + getPrimaryMode = (id?: string) => { + const mode = id + ? docsService.list.getPrimaryMode(id) + : docService.doc.getPrimaryMode(); + return (mode || DEFAULT_MODE) as DocMode; + }; + togglePrimaryMode = (id?: string) => { + const mode = id + ? docsService.list.togglePrimaryMode(id) + : docService.doc.togglePrimaryMode(); + return (mode || DEFAULT_MODE) as DocMode; + }; + onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => { + const mode$ = id + ? docsService.list.primaryMode$(id) + : docService.doc.primaryMode$; + const sub = mode$.subscribe(m => handler((m || DEFAULT_MODE) as DocMode)); + return { + dispose: sub.unsubscribe, + }; + }; + } + + const docModeExtension = DocModeExtension(new AffineDocModeService()); + + return docModeExtension; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/doc-url.ts b/packages/frontend/core/src/blocksuite/extensions/doc-url.ts new file mode 100644 index 0000000000000..5fd780866bc58 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/doc-url.ts @@ -0,0 +1,50 @@ +import { generateUrl } from '@affine/core/components/hooks/affine/use-share-url'; +import { WorkspaceServerService } from '@affine/core/modules/cloud'; +import { resolveLinkToDoc } from '@affine/core/modules/navigation/utils'; +import { WorkspaceService } from '@affine/core/modules/workspace'; +import { + GenerateDocUrlExtension, + ParseDocUrlExtension, + type ReferenceParams, +} from '@blocksuite/affine/blocks'; +import type { FrameworkProvider } from '@toeverything/infra'; + +function patchParseDocUrlExtension(framework: FrameworkProvider) { + const workspaceService = framework.get(WorkspaceService); + const ParseDocUrl = ParseDocUrlExtension({ + parseDocUrl(url) { + const info = resolveLinkToDoc(url); + if (!info || info.workspaceId !== workspaceService.workspace.id) return; + + delete info.refreshKey; + + return info; + }, + }); + + return ParseDocUrl; +} + +function patchGenerateDocUrlExtension(framework: FrameworkProvider) { + const workspaceService = framework.get(WorkspaceService); + const workspaceServerService = framework.get(WorkspaceServerService); + const GenerateDocUrl = GenerateDocUrlExtension({ + generateDocUrl(pageId: string, params?: ReferenceParams) { + return generateUrl({ + ...params, + pageId, + workspaceId: workspaceService.workspace.id, + baseUrl: workspaceServerService.server?.baseUrl ?? location.origin, + }); + }, + }); + + return GenerateDocUrl; +} + +export function patchDocUrlExtensions(framework: FrameworkProvider) { + return [ + patchParseDocUrlExtension(framework), + patchGenerateDocUrlExtension(framework), + ]; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts b/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts new file mode 100644 index 0000000000000..ee6b042e55443 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/edgeless-clipboard.ts @@ -0,0 +1,47 @@ +import { AIChatBlockSchema } from '@affine/core/blocksuite/ai/blocks'; +import { BlockServiceWatcher } from '@blocksuite/affine/block-std'; +import { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks'; +import type { BlockSnapshot } from '@blocksuite/affine/store'; + +export class EdgelessClipboardWatcher extends BlockServiceWatcher { + static override readonly flavour = 'affine:page'; + + override mounted() { + super.mounted(); + this.blockService.disposables.add( + this.blockService.specSlots.viewConnected.on(view => { + const { component } = view; + if (component instanceof EdgelessRootBlockComponent) { + const AIChatBlockFlavour = AIChatBlockSchema.model.flavour; + const createFunc = (block: BlockSnapshot) => { + const { + xywh, + scale, + messages, + sessionId, + rootDocId, + rootWorkspaceId, + } = block.props; + const blockId = component.service.crud.addBlock( + AIChatBlockFlavour, + { + xywh, + scale, + messages, + sessionId, + rootDocId, + rootWorkspaceId, + }, + component.surface.model.id + ); + return blockId; + }; + component.clipboardController.registerBlock( + AIChatBlockFlavour, + createFunc + ); + } + }) + ); + } +} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/database-block.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/database.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/database-block.ts rename to packages/frontend/core/src/blocksuite/extensions/editor-config/database.ts diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts new file mode 100644 index 0000000000000..d35a3ba34938d --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/index.ts @@ -0,0 +1,27 @@ +import { EditorSettingService } from '@affine/core/modules/editor-setting'; +import { + DatabaseConfigExtension, + EditorSettingExtension, + RootBlockConfigExtension, + ToolbarMoreMenuConfigExtension, +} from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; +import type { FrameworkProvider } from '@toeverything/infra'; + +import { createDatabaseOptionsConfig } from './database'; +import { createLinkedWidgetConfig } from './linked'; +import { createToolbarMoreMenuConfig } from './toolbar'; + +export function getEditorConfigExtension( + framework: FrameworkProvider +): ExtensionType[] { + const editorSettingService = framework.get(EditorSettingService); + return [ + EditorSettingExtension(editorSettingService.editorSetting.settingSignal), + DatabaseConfigExtension(createDatabaseOptionsConfig(framework)), + RootBlockConfigExtension({ + linkedWidget: createLinkedWidgetConfig(framework), + }), + ToolbarMoreMenuConfigExtension(createToolbarMoreMenuConfig(framework)), + ]; +} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/linked.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts rename to packages/frontend/core/src/blocksuite/extensions/editor-config/linked.ts diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/copy-as-image.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/copy-as-image.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/copy-as-image.ts rename to packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/copy-as-image.ts diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/toolbar.ts rename to packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts diff --git a/packages/frontend/core/src/blocksuite/extensions/electron-clipboard.ts b/packages/frontend/core/src/blocksuite/extensions/electron-clipboard.ts new file mode 100644 index 0000000000000..bebd8c4d637c4 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/electron-clipboard.ts @@ -0,0 +1,10 @@ +import { DesktopApiService } from '@affine/core/modules/desktop-api'; +import { NativeClipboardExtension } from '@blocksuite/affine/blocks'; +import type { FrameworkProvider } from '@toeverything/infra'; + +export function patchForClipboardInElectron(framework: FrameworkProvider) { + const desktopApi = framework.get(DesktopApiService); + return NativeClipboardExtension({ + copyAsPNG: desktopApi.handler.clipboard.copyAsPNG, + }); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/entry/enable-affine.ts b/packages/frontend/core/src/blocksuite/extensions/entry/enable-affine.ts new file mode 100644 index 0000000000000..d4b8bfca581b7 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/entry/enable-affine.ts @@ -0,0 +1,23 @@ +import type { SpecBuilder } from '@blocksuite/affine/blocks'; +import { type FrameworkProvider } from '@toeverything/infra'; + +import { buildDocDisplayMetaExtension } from '../display-meta'; +import { getEditorConfigExtension } from '../editor-config'; +import { getFontConfigExtension } from '../font-config'; +import { getTelemetryExtension } from '../telemetry'; +import { getThemeExtension } from '../theme'; + +export function enableAffineExtension( + specBuilder: SpecBuilder, + framework: FrameworkProvider +): void { + specBuilder.extend( + [ + getThemeExtension(framework), + getFontConfigExtension(), + getTelemetryExtension(), + getEditorConfigExtension(framework), + buildDocDisplayMetaExtension(framework), + ].flat() + ); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts b/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts new file mode 100644 index 0000000000000..76ffedaf44096 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/entry/enable-mobile.ts @@ -0,0 +1,113 @@ +import { createKeyboardToolbarConfig } from '@affine/core/blocksuite/extensions/keyboard-toolbar-config'; +import { + type BlockStdScope, + ConfigIdentifier, + LifeCycleWatcher, +} from '@blocksuite/affine/block-std'; +import type { + CodeBlockConfig, + ReferenceNodeConfig, + RootBlockConfig, + SpecBuilder, +} from '@blocksuite/affine/blocks'; +import { + codeToolbarWidget, + embedCardToolbarWidget, + FeatureFlagService, + formatBarWidget, + imageToolbarWidget, + ParagraphBlockService, + ReferenceNodeConfigIdentifier, + RootBlockConfigExtension, + slashMenuWidget, + surfaceRefToolbarWidget, +} from '@blocksuite/affine/blocks'; +import type { Container } from '@blocksuite/affine/global/di'; +import type { ExtensionType } from '@blocksuite/affine/store'; + +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); + } + + static override setup(di: Container) { + super.setup(di); + + // Hide reference popup on mobile. + { + const prev = di.getFactory(ReferenceNodeConfigIdentifier); + di.override(ReferenceNodeConfigIdentifier, provider => { + return { + ...prev?.(provider), + hidePopup: true, + } satisfies ReferenceNodeConfig; + }); + } + + // Hide number lines for code block on mobile. + { + const codeConfigIdentifier = ConfigIdentifier('affine:code'); + const prev = di.getFactory(codeConfigIdentifier); + di.override(codeConfigIdentifier, provider => { + return { + ...prev?.(provider), + showLineNumbers: false, + } satisfies CodeBlockConfig; + }); + } + } + + override mounted() { + // remove slash placeholder for mobile: `type / ...` + { + const paragraphService = this.std.get(ParagraphBlockService); + if (!paragraphService) return; + + paragraphService.placeholderGenerator = 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.type]; + }; + } + } +} + +const mobileExtensions: ExtensionType[] = [ + { + setup: di => { + const prev = di.getFactory(RootBlockConfigExtension.identifier); + + di.override(RootBlockConfigExtension.identifier, provider => { + return { + ...prev?.(provider), + keyboardToolbar: createKeyboardToolbarConfig(), + } satisfies RootBlockConfig; + }); + }, + }, + MobileSpecsPatches, +]; + +export function enableMobileExtension(specBuilder: SpecBuilder): void { + specBuilder.omit(formatBarWidget); + specBuilder.omit(embedCardToolbarWidget); + specBuilder.omit(slashMenuWidget); + specBuilder.omit(codeToolbarWidget); + specBuilder.omit(imageToolbarWidget); + specBuilder.omit(surfaceRefToolbarWidget); + specBuilder.extend(mobileExtensions); +} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/font-extension.ts b/packages/frontend/core/src/blocksuite/extensions/font-config.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/font-extension.ts rename to packages/frontend/core/src/blocksuite/extensions/font-config.ts diff --git a/packages/frontend/core/src/blocksuite/extensions/index.ts b/packages/frontend/core/src/blocksuite/extensions/index.ts new file mode 100644 index 0000000000000..bbc9b34f6b5a9 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/index.ts @@ -0,0 +1,2 @@ +export * from './entry/enable-affine'; +export * from './entry/enable-mobile'; diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/keyboard-toolbar.ts b/packages/frontend/core/src/blocksuite/extensions/keyboard-toolbar-config.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/keyboard-toolbar.ts rename to packages/frontend/core/src/blocksuite/extensions/keyboard-toolbar-config.ts diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.css.ts b/packages/frontend/core/src/blocksuite/extensions/note-config/edgeless-note-header.css.ts similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.css.ts rename to packages/frontend/core/src/blocksuite/extensions/note-config/edgeless-note-header.css.ts diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx b/packages/frontend/core/src/blocksuite/extensions/note-config/edgeless-note-header.tsx similarity index 100% rename from packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/widgets/edgeless-note-header.tsx rename to packages/frontend/core/src/blocksuite/extensions/note-config/edgeless-note-header.tsx diff --git a/packages/frontend/core/src/blocksuite/extensions/note-config/index.tsx b/packages/frontend/core/src/blocksuite/extensions/note-config/index.tsx new file mode 100644 index 0000000000000..e2c267ccc095c --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/note-config/index.tsx @@ -0,0 +1,27 @@ +import type { ElementOrFactory } from '@affine/component'; +import { JournalService } from '@affine/core/modules/journal'; +import { NoteConfigExtension } from '@blocksuite/affine/blocks'; +import type { FrameworkProvider } from '@toeverything/infra'; +import { html, type TemplateResult } from 'lit'; + +import { BlocksuiteEditorJournalDocTitle } from '../../block-suite-editor/journal-doc-title'; +import { EdgelessNoteHeader } from './edgeless-note-header'; + +export function patchForEdgelessNoteConfig( + framework: FrameworkProvider, + reactToLit: (element: ElementOrFactory) => TemplateResult +) { + return NoteConfigExtension({ + edgelessNoteHeader: ({ note }) => + reactToLit(), + pageBlockTitle: ({ note }) => { + const journalService = framework.get(JournalService); + const isJournal = !!journalService.journalDate$(note.doc.id).value; + if (isJournal) { + return reactToLit(); + } else { + return html``; + } + }, + }); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/notification-service.tsx b/packages/frontend/core/src/blocksuite/extensions/notification-service.tsx new file mode 100644 index 0000000000000..a2cc2dd8c4e7e --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/notification-service.tsx @@ -0,0 +1,124 @@ +import { + Input, + notify, + toast, + type ToastOptions, + toReactNode, + type useConfirmModal, +} from '@affine/component'; +import { NotificationExtension } from '@blocksuite/affine/blocks'; + +export function patchNotificationService({ + closeConfirmModal, + openConfirmModal, +}: ReturnType) { + return NotificationExtension({ + confirm: async ({ title, message, confirmText, cancelText, abort }) => { + return new Promise(resolve => { + openConfirmModal({ + title: toReactNode(title), + description: toReactNode(message), + confirmText, + confirmButtonOptions: { + variant: 'primary', + }, + cancelText, + onConfirm: () => { + resolve(true); + }, + onCancel: () => { + resolve(false); + }, + }); + abort?.addEventListener('abort', () => { + resolve(false); + closeConfirmModal(); + }); + }); + }, + prompt: async ({ + title, + message, + confirmText, + placeholder, + cancelText, + autofill, + abort, + }) => { + return new Promise(resolve => { + let value = autofill || ''; + const description = ( +
+ {toReactNode(message)} + (value = e)} + /> +
+ ); + openConfirmModal({ + title: toReactNode(title), + description: description, + confirmText: confirmText ?? 'Confirm', + confirmButtonOptions: { + variant: 'primary', + }, + cancelText: cancelText ?? 'Cancel', + onConfirm: () => { + resolve(value); + }, + onCancel: () => { + resolve(null); + }, + autoFocusConfirm: false, + }); + abort?.addEventListener('abort', () => { + resolve(null); + closeConfirmModal(); + }); + }); + }, + toast: (message: string, options: ToastOptions) => { + return toast(message, options); + }, + notify: notification => { + const accentToNotify = { + error: notify.error, + success: notify.success, + warning: notify.warning, + info: notify, + }; + + const fn = accentToNotify[notification.accent || 'info']; + if (!fn) { + throw new Error('Invalid notification accent'); + } + + const toastId = fn( + { + title: toReactNode(notification.title), + message: toReactNode(notification.message), + footer: toReactNode(notification.footer), + action: notification.action?.onClick + ? { + label: toReactNode(notification.action?.label), + onClick: notification.action.onClick, + } + : undefined, + onDismiss: notification.onClose, + }, + { + duration: notification.duration || 0, + onDismiss: notification.onClose, + onAutoClose: notification.onClose, + } + ); + + notification.abort?.addEventListener('abort', () => { + notify.dismiss(toastId); + }); + }, + }); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/open-doc.ts b/packages/frontend/core/src/blocksuite/extensions/open-doc.ts new file mode 100644 index 0000000000000..4b5444deb69bf --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/open-doc.ts @@ -0,0 +1,43 @@ +import { I18n } from '@affine/i18n'; +import type { + OpenDocConfig, + OpenDocConfigItem, +} from '@blocksuite/affine/blocks'; +import { OpenDocExtension } from '@blocksuite/affine/blocks'; +import { + CenterPeekIcon, + ExpandFullIcon, + OpenInNewIcon, + SplitViewIcon, +} from '@blocksuite/icons/lit'; + +export function patchOpenDocExtension() { + const openDocConfig: OpenDocConfig = { + items: [ + { + type: 'open-in-active-view', + label: I18n['com.affine.peek-view-controls.open-doc'](), + icon: ExpandFullIcon(), + }, + BUILD_CONFIG.isElectron + ? { + type: 'open-in-new-view', + label: + I18n['com.affine.peek-view-controls.open-doc-in-split-view'](), + icon: SplitViewIcon(), + } + : null, + { + type: 'open-in-new-tab', + label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](), + icon: OpenInNewIcon(), + }, + { + type: 'open-in-center-peek', + label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](), + icon: CenterPeekIcon(), + }, + ].filter((item): item is OpenDocConfigItem => item !== null), + }; + return OpenDocExtension(openDocConfig); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/peek-view-service.ts b/packages/frontend/core/src/blocksuite/extensions/peek-view-service.ts new file mode 100644 index 0000000000000..8d1e2ec8ce3e3 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/peek-view-service.ts @@ -0,0 +1,36 @@ +import type { PeekViewService } from '@affine/core/modules/peek-view'; +import { DebugLogger } from '@affine/debug'; +import type { + PeekOptions, + PeekViewService as BSPeekViewService, +} from '@blocksuite/affine/blocks'; +import { PeekViewExtension } from '@blocksuite/affine/blocks'; +import type { TemplateResult } from 'lit'; + +const logger = new DebugLogger('affine::patch-peek-view-service'); + +export function patchPeekViewService(service: PeekViewService) { + return PeekViewExtension({ + peek: ( + element: { + target: HTMLElement; + docId: string; + blockIds?: string[]; + template?: TemplateResult; + }, + options?: PeekOptions + ) => { + logger.debug('center peek', element); + const { template, target, ...props } = element; + + return service.peekView.open( + { + element: target, + docRef: props, + }, + template, + options?.abortSignal + ); + }, + } satisfies BSPeekViewService); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/quick-search-service.ts b/packages/frontend/core/src/blocksuite/extensions/quick-search-service.ts new file mode 100644 index 0000000000000..5fbb5612fc8a4 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/quick-search-service.ts @@ -0,0 +1,187 @@ +import { DocsService } from '@affine/core/modules/doc'; +import { EditorSettingService } from '@affine/core/modules/editor-setting'; +import { + CreationQuickSearchSession, + DocsQuickSearchSession, + LinksQuickSearchSession, + QuickSearchService, + RecentDocsQuickSearchSession, +} from '@affine/core/modules/quicksearch'; +import { ExternalLinksQuickSearchSession } from '@affine/core/modules/quicksearch/impls/external-links'; +import { JournalsQuickSearchSession } from '@affine/core/modules/quicksearch/impls/journals'; +import { track } from '@affine/track'; +import { + BlockServiceWatcher, + type WidgetComponent, +} from '@blocksuite/affine/block-std'; +import type { QuickSearchResult } from '@blocksuite/affine/blocks'; +import { + AffineSlashMenuWidget, + insertLinkByQuickSearchCommand, + QuickSearchExtension, +} from '@blocksuite/affine/blocks'; +import { Text } from '@blocksuite/affine/store'; +import type { FrameworkProvider } from '@toeverything/infra'; +import { pick } from 'lodash-es'; + +import type { DocProps } from '../initialization'; + +export function patchQuickSearchService(framework: FrameworkProvider) { + const QuickSearch = QuickSearchExtension({ + async openQuickSearch() { + let searchResult: QuickSearchResult = null; + searchResult = await new Promise((resolve, reject) => + framework.get(QuickSearchService).quickSearch.show( + [ + framework.get(RecentDocsQuickSearchSession), + framework.get(CreationQuickSearchSession), + framework.get(DocsQuickSearchSession), + framework.get(LinksQuickSearchSession), + framework.get(ExternalLinksQuickSearchSession), + framework.get(JournalsQuickSearchSession), + ], + result => { + if (result === null) { + resolve(null); + return; + } + + if (result.source === 'docs') { + resolve({ + docId: result.payload.docId, + }); + return; + } + + if (result.source === 'recent-doc') { + resolve({ + docId: result.payload.docId, + }); + return; + } + + if (result.source === 'link') { + resolve({ + docId: result.payload.docId, + params: pick(result.payload, [ + 'mode', + 'blockIds', + 'elementIds', + ]), + }); + return; + } + + if (result.source === 'date-picker') { + result.payload + .getDocId() + .then(docId => { + if (docId) { + resolve({ docId }); + } + }) + .catch(reject); + return; + } + + if (result.source === 'external-link') { + const externalUrl = result.payload.url; + resolve({ externalUrl }); + return; + } + + if (result.source === 'creation') { + const docsService = framework.get(DocsService); + const editorSettingService = framework.get(EditorSettingService); + const mode = + result.id === 'creation:create-edgeless' ? 'edgeless' : 'page'; + const docProps: DocProps = { + page: { title: new Text(result.payload.title) }, + note: editorSettingService.editorSetting.get('affine:note'), + }; + const newDoc = docsService.createDoc({ + primaryMode: mode, + docProps, + }); + + resolve({ docId: newDoc.id }); + return; + } + }, + { + label: { + i18nKey: 'com.affine.cmdk.insert-links', + }, + placeholder: { + i18nKey: 'com.affine.cmdk.docs.placeholder', + }, + } + ) + ); + + return searchResult; + }, + }); + + const SlashMenuQuickSearchExtension = patchSpecService( + 'affine:page', + (component: WidgetComponent) => { + if (component instanceof AffineSlashMenuWidget) { + component.config.items.forEach(item => { + if ( + 'action' in item && + (item.name === 'Linked Doc' || item.name === 'Link') + ) { + item.action = async ({ rootComponent }) => { + const [success, { insertedLinkType }] = + rootComponent.std.command.exec(insertLinkByQuickSearchCommand); + + if (!success) return; + + insertedLinkType + ?.then(type => { + const flavour = type?.flavour; + if (!flavour) return; + + if (flavour === 'affine:bookmark') { + track.doc.editor.slashMenu.bookmark(); + return; + } + + if (flavour === 'affine:embed-linked-doc') { + track.doc.editor.slashMenu.linkDoc({ + control: 'linkDoc', + }); + return; + } + }) + .catch(console.error); + }; + } + }); + } + } + ); + return [QuickSearch, SlashMenuQuickSearchExtension]; +} + +function patchSpecService( + flavour: string, + onWidgetConnected?: (component: WidgetComponent) => void +) { + class TempServiceWatcher extends BlockServiceWatcher { + static override readonly flavour = flavour; + override mounted() { + super.mounted(); + const disposableGroup = this.blockService.disposables; + if (onWidgetConnected) { + disposableGroup.add( + this.blockService.specSlots.widgetConnected.on(({ component }) => { + onWidgetConnected(component); + }) + ); + } + } + } + return TempServiceWatcher; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts b/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts new file mode 100644 index 0000000000000..24f091b56bc46 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/reference-renderer.ts @@ -0,0 +1,23 @@ +import type { ElementOrFactory } from '@affine/component'; +import type { AffineReference } from '@blocksuite/affine/blocks'; +import { ReferenceNodeConfigExtension } from '@blocksuite/affine/blocks'; +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, + reactRenderer: ReferenceReactRenderer +): ExtensionType { + const customContent = (reference: AffineReference) => { + const node = reactRenderer(reference); + return reactToLit(node); + }; + + return ReferenceNodeConfigExtension({ + customContent, + }); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/side-bar-service.ts b/packages/frontend/core/src/blocksuite/extensions/side-bar-service.ts new file mode 100644 index 0000000000000..c72de0768b5fd --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/side-bar-service.ts @@ -0,0 +1,20 @@ +import { WorkbenchService } from '@affine/core/modules/workbench'; +import { SidebarExtension } from '@blocksuite/affine/blocks'; +import type { FrameworkProvider } from '@toeverything/infra'; + +export function patchSideBarService(framework: FrameworkProvider) { + const { workbench } = framework.get(WorkbenchService); + + return SidebarExtension({ + open: (tabId?: string) => { + workbench.openSidebar(); + workbench.activeView$.value.activeSidebarTab(tabId ?? null); + }, + close: () => { + workbench.closeSidebar(); + }, + getTabIds: () => { + return workbench.activeView$.value.sidebarTabs$.value.map(tab => tab.id); + }, + }); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/telemetry.ts b/packages/frontend/core/src/blocksuite/extensions/telemetry.ts new file mode 100644 index 0000000000000..ccda0a539ba45 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/telemetry.ts @@ -0,0 +1,21 @@ +import { mixpanel } from '@affine/track'; +import { + type TelemetryEventMap, + TelemetryProvider, +} from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; + +export function getTelemetryExtension(): ExtensionType { + return { + setup: di => { + di.addImpl(TelemetryProvider, () => ({ + track: ( + eventName: T, + props: TelemetryEventMap[T] + ) => { + mixpanel.track(eventName as string, props as Record); + }, + })); + }, + }; +} diff --git a/packages/frontend/core/src/blocksuite/extensions/theme.ts b/packages/frontend/core/src/blocksuite/extensions/theme.ts new file mode 100644 index 0000000000000..cf75c90dd8b72 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/extensions/theme.ts @@ -0,0 +1,94 @@ +import { DocService, DocsService } from '@affine/core/modules/doc'; +import { AppThemeService } from '@affine/core/modules/theme'; +import { LifeCycleWatcher, StdIdentifier } from '@blocksuite/affine/block-std'; +import type { Signal, ThemeExtension } from '@blocksuite/affine/blocks'; +import { + ColorScheme, + createSignalFromObservable, + ThemeExtensionIdentifier, +} from '@blocksuite/affine/blocks'; +import type { Container } from '@blocksuite/affine/global/di'; +import { type FrameworkProvider } from '@toeverything/infra'; +import type { Observable } from 'rxjs'; +import { combineLatest, map } from 'rxjs'; + +export function getThemeExtension( + framework: FrameworkProvider +): typeof LifeCycleWatcher { + class AffineThemeExtension + extends LifeCycleWatcher + implements ThemeExtension + { + static override readonly key = 'affine-theme'; + + private readonly themes: Map> = new Map(); + + protected readonly disposables: (() => void)[] = []; + + static override setup(di: Container) { + super.setup(di); + di.override(ThemeExtensionIdentifier, AffineThemeExtension, [ + StdIdentifier, + ]); + } + + getAppTheme() { + const keyName = 'app-theme'; + const cache = this.themes.get(keyName); + if (cache) return cache; + + const theme$: Observable = framework + .get(AppThemeService) + .appTheme.theme$.map(theme => { + return theme === ColorScheme.Dark + ? ColorScheme.Dark + : ColorScheme.Light; + }); + const { signal: themeSignal, cleanup } = + createSignalFromObservable(theme$, ColorScheme.Light); + this.disposables.push(cleanup); + this.themes.set(keyName, themeSignal); + return themeSignal; + } + + getEdgelessTheme(docId?: string) { + const doc = + (docId && framework.get(DocsService).list.doc$(docId).getValue()) || + framework.get(DocService).doc; + + const cache = this.themes.get(doc.id); + if (cache) return cache; + + const appTheme$ = framework.get(AppThemeService).appTheme.theme$; + const docTheme$ = doc.properties$.map( + props => props.edgelessColorTheme || 'system' + ); + const theme$: Observable = combineLatest([ + appTheme$, + docTheme$, + ]).pipe( + map(([appTheme, docTheme]) => { + const theme = docTheme === 'system' ? appTheme : docTheme; + return theme === ColorScheme.Dark + ? ColorScheme.Dark + : ColorScheme.Light; + }) + ); + const { signal: themeSignal, cleanup } = + createSignalFromObservable(theme$, ColorScheme.Light); + this.disposables.push(cleanup); + this.themes.set(doc.id, themeSignal); + return themeSignal; + } + + override unmounted() { + this.dispose(); + } + + dispose() { + this.disposables.forEach(dispose => dispose()); + } + } + + return AffineThemeExtension; +}