feat(core): impl doc display meta extension (#9165)
Closes: [BS-2111](https://linear.app/affine-design/issue/BS-2111/定义和实现-docdisplaymetaextension) Upstreams: https://github.com/toeverything/blocksuite/pull/8953 https://github.com/user-attachments/assets/008d7433-efef-47c4-8189-9bc288e61199
This commit is contained in:
@@ -3,7 +3,6 @@ import { JournalService } from '@affine/core/modules/journal';
|
|||||||
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
import { PeekViewService } from '@affine/core/modules/peek-view/services/peek-view';
|
||||||
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
import { useInsidePeekView } from '@affine/core/modules/peek-view/view/modal-container';
|
||||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import { track } from '@affine/track';
|
import { track } from '@affine/track';
|
||||||
import type { DocMode } from '@blocksuite/affine/blocks';
|
import type { DocMode } from '@blocksuite/affine/blocks';
|
||||||
import type { DocCollection } from '@blocksuite/affine/store';
|
import type { DocCollection } from '@blocksuite/affine/store';
|
||||||
@@ -30,7 +29,7 @@ import * as styles from './styles.css';
|
|||||||
interface AffinePageReferenceProps {
|
interface AffinePageReferenceProps {
|
||||||
pageId: string;
|
pageId: string;
|
||||||
params?: URLSearchParams;
|
params?: URLSearchParams;
|
||||||
title?: string | null; // title alias
|
title?: string; // title alias
|
||||||
className?: string;
|
className?: string;
|
||||||
Icon?: ComponentType;
|
Icon?: ComponentType;
|
||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
@@ -44,7 +43,6 @@ function AffinePageReferenceInner({
|
|||||||
}: AffinePageReferenceProps) {
|
}: AffinePageReferenceProps) {
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const docsService = useService(DocsService);
|
const docsService = useService(DocsService);
|
||||||
const i18n = useI18n();
|
|
||||||
|
|
||||||
let referenceWithMode: DocMode | null = null;
|
let referenceWithMode: DocMode | null = null;
|
||||||
let referenceToNode = false;
|
let referenceToNode = false;
|
||||||
@@ -74,18 +72,10 @@ function AffinePageReferenceInner({
|
|||||||
|
|
||||||
const notFound = !useLiveData(docsService.list.doc$(pageId));
|
const notFound = !useLiveData(docsService.list.doc$(pageId));
|
||||||
|
|
||||||
const docTitle = useLiveData(
|
title = useLiveData(
|
||||||
docDisplayMetaService.title$(pageId, { reference: true })
|
docDisplayMetaService.title$(pageId, { title, reference: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
if (notFound) {
|
|
||||||
title = i18n.t('com.affine.notFoundPage.title');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!title) {
|
|
||||||
title = i18n.t(docTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={notFound ? styles.notFound : ''}>
|
<span className={notFound ? styles.notFound : ''}>
|
||||||
<Icon className={styles.pageReferenceIcon} />
|
<Icon className={styles.pageReferenceIcon} />
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
AIEdgelessRootBlockSpec,
|
AIEdgelessRootBlockSpec,
|
||||||
AIPageRootBlockSpec,
|
AIPageRootBlockSpec,
|
||||||
} from '@affine/core/blocksuite/presets/ai';
|
} from '@affine/core/blocksuite/presets/ai';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||||
import { AppThemeService } from '@affine/core/modules/theme';
|
import { AppThemeService } from '@affine/core/modules/theme';
|
||||||
import { mixpanel } from '@affine/track';
|
import { mixpanel } from '@affine/track';
|
||||||
@@ -12,12 +13,15 @@ import {
|
|||||||
StdIdentifier,
|
StdIdentifier,
|
||||||
} from '@blocksuite/affine/block-std';
|
} from '@blocksuite/affine/block-std';
|
||||||
import type {
|
import type {
|
||||||
|
DocDisplayMetaExtension,
|
||||||
|
DocDisplayMetaParams,
|
||||||
RootBlockConfig,
|
RootBlockConfig,
|
||||||
TelemetryEventMap,
|
TelemetryEventMap,
|
||||||
ThemeExtension,
|
ThemeExtension,
|
||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import {
|
import {
|
||||||
ColorScheme,
|
ColorScheme,
|
||||||
|
DocDisplayMetaProvider,
|
||||||
EdgelessBuiltInManager,
|
EdgelessBuiltInManager,
|
||||||
EdgelessRootBlockSpec,
|
EdgelessRootBlockSpec,
|
||||||
EdgelessToolExtension,
|
EdgelessToolExtension,
|
||||||
@@ -29,16 +33,19 @@ import {
|
|||||||
} from '@blocksuite/affine/blocks';
|
} from '@blocksuite/affine/blocks';
|
||||||
import {
|
import {
|
||||||
createSignalFromObservable,
|
createSignalFromObservable,
|
||||||
|
referenceToNode,
|
||||||
type Signal,
|
type Signal,
|
||||||
SpecProvider,
|
SpecProvider,
|
||||||
} from '@blocksuite/affine-shared/utils';
|
} from '@blocksuite/affine-shared/utils';
|
||||||
import type { Container } from '@blocksuite/global/di';
|
import type { Container } from '@blocksuite/global/di';
|
||||||
|
import { LinkedPageIcon, PageIcon } from '@blocksuite/icons/lit';
|
||||||
import {
|
import {
|
||||||
DocService,
|
DocService,
|
||||||
DocsService,
|
DocsService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
type FrameworkProvider,
|
type FrameworkProvider,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import type { TemplateResult } from 'lit';
|
||||||
import type { Observable } from 'rxjs';
|
import type { Observable } from 'rxjs';
|
||||||
import { combineLatest, map } from 'rxjs';
|
import { combineLatest, map } from 'rxjs';
|
||||||
|
|
||||||
@@ -141,6 +148,88 @@ function getThemeExtension(framework: FrameworkProvider) {
|
|||||||
return AffineThemeExtension;
|
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<TemplateResult> {
|
||||||
|
const icon$ = docDisplayMetaService
|
||||||
|
.icon$(docId, {
|
||||||
|
type: 'lit',
|
||||||
|
reference: referenced,
|
||||||
|
hasTitleAlias: Boolean(title),
|
||||||
|
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<string> {
|
||||||
|
const title$ = docDisplayMetaService.title$(docId, {
|
||||||
|
title,
|
||||||
|
reference: referenced,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { signal: titleSignal, cleanup } =
|
||||||
|
createSignalFromObservable<string>(title$, title);
|
||||||
|
|
||||||
|
this.disposables.push(cleanup);
|
||||||
|
|
||||||
|
return titleSignal;
|
||||||
|
}
|
||||||
|
|
||||||
|
override unmounted() {
|
||||||
|
this.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AffineDocDisplayMetaService;
|
||||||
|
}
|
||||||
|
|
||||||
function getEditorConfigExtension(
|
function getEditorConfigExtension(
|
||||||
framework: FrameworkProvider
|
framework: FrameworkProvider
|
||||||
): ExtensionType[] {
|
): ExtensionType[] {
|
||||||
@@ -184,6 +273,7 @@ export function createPageRootBlockSpec(
|
|||||||
getFontConfigExtension(),
|
getFontConfigExtension(),
|
||||||
getTelemetryExtension(),
|
getTelemetryExtension(),
|
||||||
getEditorConfigExtension(framework),
|
getEditorConfigExtension(framework),
|
||||||
|
buildDocDisplayMetaExtension(framework),
|
||||||
].flat();
|
].flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,5 +291,6 @@ export function createEdgelessRootBlockSpec(
|
|||||||
getFontConfigExtension(),
|
getFontConfigExtension(),
|
||||||
getTelemetryExtension(),
|
getTelemetryExtension(),
|
||||||
getEditorConfigExtension(framework),
|
getEditorConfigExtension(framework),
|
||||||
|
buildDocDisplayMetaExtension(framework),
|
||||||
].flat();
|
].flat();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,7 +312,6 @@ export function patchDocModeService(
|
|||||||
return (mode || DEFAULT_MODE) as DocMode;
|
return (mode || DEFAULT_MODE) as DocMode;
|
||||||
};
|
};
|
||||||
onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => {
|
onPrimaryModeChange = (handler: (mode: DocMode) => void, id?: string) => {
|
||||||
// eslint-disable-next-line rxjs/finnish
|
|
||||||
const mode$ = id
|
const mode$ = id
|
||||||
? docsService.list.primaryMode$(id)
|
? docsService.list.primaryMode$(id)
|
||||||
: docService.doc.primaryMode$;
|
: docService.doc.primaryMode$;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
WorkbenchLink,
|
WorkbenchLink,
|
||||||
type WorkbenchLinkProps,
|
type WorkbenchLinkProps,
|
||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import type { DocMeta } from '@blocksuite/affine/store';
|
import type { DocMeta } from '@blocksuite/affine/store';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -48,13 +47,9 @@ export const DocCard = forwardRef<HTMLAnchorElement, DocCardProps>(
|
|||||||
outerRef
|
outerRef
|
||||||
) {
|
) {
|
||||||
const containerRef = useRef<HTMLAnchorElement | null>(null);
|
const containerRef = useRef<HTMLAnchorElement | null>(null);
|
||||||
const t = useI18n();
|
|
||||||
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
const favAdapter = useService(CompatibleFavoriteItemsAdapter);
|
||||||
const docDisplayService = useService(DocDisplayMetaService);
|
const docDisplayService = useService(DocDisplayMetaService);
|
||||||
const titleInfo = useLiveData(docDisplayService.title$(meta.id));
|
const title = useLiveData(docDisplayService.title$(meta.id));
|
||||||
const title =
|
|
||||||
typeof titleInfo === 'string' ? titleInfo : t[titleInfo.i18nKey]();
|
|
||||||
|
|
||||||
const favorited = useLiveData(favAdapter.isFavorite$(meta.id, 'doc'));
|
const favorited = useLiveData(favAdapter.isFavorite$(meta.id, 'doc'));
|
||||||
|
|
||||||
const toggleFavorite = useCatchEventCallback(
|
const toggleFavorite = useCatchEventCallback(
|
||||||
|
|||||||
@@ -19,11 +19,9 @@ const DocIcon = ({ docId }: { docId: string }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const DocLabel = ({ docId }: { docId: string }) => {
|
const DocLabel = ({ docId }: { docId: string }) => {
|
||||||
const t = useI18n();
|
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const label = useLiveData(docDisplayMetaService.title$(docId));
|
const label = useLiveData(docDisplayMetaService.title$(docId));
|
||||||
|
return label;
|
||||||
return typeof label === 'string' ? label : t[label.i18nKey]();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocSelectorDialog = ({
|
export const DocSelectorDialog = ({
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { EditorService } from '@affine/core/modules/editor';
|
|||||||
import { JournalService } from '@affine/core/modules/journal';
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
import { i18nTime, useI18n } from '@affine/i18n';
|
import { i18nTime } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
BookmarkBlockService,
|
BookmarkBlockService,
|
||||||
customImageProxyMiddleware,
|
customImageProxyMiddleware,
|
||||||
@@ -240,14 +240,11 @@ const MobileDetailPage = ({
|
|||||||
pageId: string;
|
pageId: string;
|
||||||
date?: string;
|
date?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const t = useI18n();
|
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
const journalService = useService(JournalService);
|
const journalService = useService(JournalService);
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const [showTitle, setShowTitle] = useState(checkShowTitle);
|
const [showTitle, setShowTitle] = useState(checkShowTitle);
|
||||||
const titleInfo = useLiveData(docDisplayMetaService.title$(pageId));
|
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
||||||
const title =
|
|
||||||
typeof titleInfo === 'string' ? titleInfo : t[titleInfo.i18nKey]();
|
|
||||||
|
|
||||||
const allJournalDates = useLiveData(journalService.allJournalDates$);
|
const allJournalDates = useLiveData(journalService.allJournalDates$);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
WorkspaceScope,
|
WorkspaceScope,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { I18nService } from '../i18n';
|
||||||
import { JournalService } from '../journal';
|
import { JournalService } from '../journal';
|
||||||
import { DocDisplayMetaService } from './services/doc-display-meta';
|
import { DocDisplayMetaService } from './services/doc-display-meta';
|
||||||
|
|
||||||
@@ -17,5 +18,6 @@ export function configureDocDisplayMetaModule(framework: Framework) {
|
|||||||
JournalService,
|
JournalService,
|
||||||
DocsService,
|
DocsService,
|
||||||
FeatureFlagService,
|
FeatureFlagService,
|
||||||
|
I18nService,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { LiveData, Service } from '@toeverything/infra';
|
|||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
import type { I18nService } from '../../i18n';
|
||||||
import type { JournalService } from '../../journal';
|
import type { JournalService } from '../../journal';
|
||||||
|
|
||||||
type IconType = 'rc' | 'lit';
|
type IconType = 'rc' | 'lit';
|
||||||
@@ -52,6 +53,7 @@ interface DocDisplayIconOptions<T extends IconType> {
|
|||||||
}
|
}
|
||||||
interface DocDisplayTitleOptions {
|
interface DocDisplayTitleOptions {
|
||||||
originalTitle?: string;
|
originalTitle?: string;
|
||||||
|
title?: string; // title alias
|
||||||
reference?: boolean;
|
reference?: boolean;
|
||||||
/**
|
/**
|
||||||
* @default true
|
* @default true
|
||||||
@@ -90,7 +92,8 @@ export class DocDisplayMetaService extends Service {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly journalService: JournalService,
|
private readonly journalService: JournalService,
|
||||||
private readonly docsService: DocsService,
|
private readonly docsService: DocsService,
|
||||||
private readonly featureFlagService: FeatureFlagService
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
|
private readonly i18nService: I18nService
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@@ -149,7 +152,7 @@ export class DocDisplayMetaService extends Service {
|
|||||||
|
|
||||||
// journal icon
|
// journal icon
|
||||||
const journalDate = this._toDayjs(
|
const journalDate = this._toDayjs(
|
||||||
this.journalService.journalDate$(docId).value
|
get(this.journalService.journalDate$(docId))
|
||||||
);
|
);
|
||||||
if (journalDate) {
|
if (journalDate) {
|
||||||
return this.getJournalIcon(journalDate, options);
|
return this.getJournalIcon(journalDate, options);
|
||||||
@@ -178,31 +181,48 @@ export class DocDisplayMetaService extends Service {
|
|||||||
|
|
||||||
title$(docId: string, options?: DocDisplayTitleOptions) {
|
title$(docId: string, options?: DocDisplayTitleOptions) {
|
||||||
return LiveData.computed(get => {
|
return LiveData.computed(get => {
|
||||||
|
const enableEmojiIcon =
|
||||||
|
get(this.featureFlagService.flags.enable_emoji_doc_icon.$) &&
|
||||||
|
options?.enableEmojiIcon !== false;
|
||||||
|
const lng = get(this.i18nService.i18n.currentLanguageKey$);
|
||||||
const doc = get(this.docsService.list.doc$(docId));
|
const doc = get(this.docsService.list.doc$(docId));
|
||||||
const docTitle = doc ? get(doc.title$) : undefined;
|
|
||||||
|
|
||||||
|
// title alias
|
||||||
|
if (options?.title) {
|
||||||
|
return enableEmojiIcon
|
||||||
|
? extractEmojiIcon(options.title).rest
|
||||||
|
: options.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc) {
|
||||||
|
return this.i18nService.i18n.i18next.t(
|
||||||
|
'com.affine.notFoundPage.title',
|
||||||
|
{ lng }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// journal title
|
||||||
const journalDateString = get(this.journalService.journalDate$(docId));
|
const journalDateString = get(this.journalService.journalDate$(docId));
|
||||||
|
|
||||||
// journal
|
|
||||||
if (journalDateString) {
|
if (journalDateString) {
|
||||||
return i18nTime(journalDateString, { absolute: { accuracy: 'day' } });
|
return i18nTime(journalDateString, { absolute: { accuracy: 'day' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// original title
|
||||||
if (options?.originalTitle) return options.originalTitle;
|
if (options?.originalTitle) return options.originalTitle;
|
||||||
|
|
||||||
|
const docTitle = get(doc.title$);
|
||||||
|
|
||||||
// empty title
|
// empty title
|
||||||
if (!docTitle) return { i18nKey: 'Untitled' } as const;
|
if (!docTitle) {
|
||||||
|
return this.i18nService.i18n.i18next.t('Untitled', { lng });
|
||||||
|
}
|
||||||
|
|
||||||
// reference
|
// reference
|
||||||
if (options?.reference) return docTitle;
|
if (options?.reference) return docTitle;
|
||||||
|
|
||||||
// check emoji
|
// emoji icon
|
||||||
const enableEmojiIcon =
|
|
||||||
get(this.featureFlagService.flags.enable_emoji_doc_icon.$) &&
|
|
||||||
options?.enableEmojiIcon !== false;
|
|
||||||
if (enableEmojiIcon) {
|
if (enableEmojiIcon) {
|
||||||
const { rest } = extractEmojiIcon(docTitle);
|
return extractEmojiIcon(docTitle).rest;
|
||||||
return rest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// default
|
// default
|
||||||
|
|||||||
Reference in New Issue
Block a user