diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index bb84e4427d..8d3d50056f 100644 --- a/packages/common/infra/src/modules/feature-flag/constant.ts +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -104,6 +104,17 @@ export const AFFINE_FLAGS = { configurable: true, defaultState: true, }, + enable_emoji_doc_icon: { + category: 'affine', + displayName: 'Emoji Doc Icon', + description: + 'Once enabled, you can use an emoji as the page icon. When the first character of the folder name is an emoji, it will be extracted and used as its icon.', + feedbackType: 'discord', + feedbackLink: + 'https://discord.com/channels/959027316334407691/1280014319865696351', + configurable: true, + defaultState: true, + }, enable_editor_settings: { category: 'affine', displayName: 'Editor Settings', diff --git a/packages/frontend/core/src/components/affine/reference-link/index.tsx b/packages/frontend/core/src/components/affine/reference-link/index.tsx index 6c58bec9ba..eb083cd430 100644 --- a/packages/frontend/core/src/components/affine/reference-link/index.tsx +++ b/packages/frontend/core/src/components/affine/reference-link/index.tsx @@ -52,7 +52,9 @@ export function AffinePageReference({ referenceToNode: linkToNode, }) ); - const title = useLiveData(docDisplayMetaService.title$(pageId)); + const title = useLiveData( + docDisplayMetaService.title$(pageId, { reference: true }) + ); const el = ( <> diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts index 3371ebf1f2..124569e3d4 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/specs/custom/widgets/linked.ts @@ -34,7 +34,9 @@ export function createLinkedWidgetConfig( return !meta.trash; }) .map(meta => { - const title = docDisplayMetaService.title$(meta.id).value; + const title = docDisplayMetaService.title$(meta.id, { + reference: true, + }).value; return { ...meta, title: typeof title === 'string' ? title : I18n[title.key](), diff --git a/packages/frontend/core/src/modules/doc-display-meta/index.ts b/packages/frontend/core/src/modules/doc-display-meta/index.ts index 179fedf223..90422c98fb 100644 --- a/packages/frontend/core/src/modules/doc-display-meta/index.ts +++ b/packages/frontend/core/src/modules/doc-display-meta/index.ts @@ -1,5 +1,6 @@ import { DocsService, + FeatureFlagService, type Framework, WorkspaceScope, } from '@toeverything/infra'; @@ -12,5 +13,9 @@ export { DocDisplayMetaService }; export function configureDocDisplayMetaModule(framework: Framework) { framework .scope(WorkspaceScope) - .service(DocDisplayMetaService, [WorkspacePropertiesAdapter, DocsService]); + .service(DocDisplayMetaService, [ + WorkspacePropertiesAdapter, + DocsService, + FeatureFlagService, + ]); } diff --git a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts index 82a3b19a4c..4aa528283b 100644 --- a/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts +++ b/packages/frontend/core/src/modules/doc-display-meta/services/doc-display-meta.ts @@ -1,3 +1,4 @@ +import { extractEmojiIcon } from '@affine/core/utils'; import { i18nTime } from '@affine/i18n'; import { BlockLinkIcon as LitBlockLinkIcon, @@ -19,7 +20,11 @@ import { TomorrowIcon, YesterdayIcon, } from '@blocksuite/icons/rc'; -import type { DocRecord, DocsService } from '@toeverything/infra'; +import type { + DocRecord, + DocsService, + FeatureFlagService, +} from '@toeverything/infra'; import { LiveData, Service } from '@toeverything/infra'; import type { Dayjs } from 'dayjs'; import dayjs from 'dayjs'; @@ -37,6 +42,18 @@ interface DocDisplayIconOptions { mode?: 'edgeless' | 'page'; reference?: boolean; referenceToNode?: boolean; + /** + * @default true + */ + enableEmojiIcon?: boolean; +} +interface DocDisplayTitleOptions { + originalTitle?: string; + reference?: boolean; + /** + * @default true + */ + enableEmojiIcon?: boolean; } const rcIcons = { @@ -67,7 +84,8 @@ const icons = { rc: rcIcons, lit: litIcons } as { export class DocDisplayMetaService extends Service { constructor( private readonly propertiesAdapter: WorkspacePropertiesAdapter, - private readonly docsService: DocsService + private readonly docsService: DocsService, + private readonly featureFlagService: FeatureFlagService ) { super(); } @@ -80,6 +98,7 @@ export class DocDisplayMetaService extends Service { return LiveData.computed(get => { const doc = get(this.docsService.list.doc$(docId)); + const title = doc ? get(doc.title$) : ''; const mode = doc ? get(doc.primaryMode$) : undefined; const finalMode = options?.mode ?? mode ?? 'page'; const referenceToNode = !!(options?.reference && options.referenceToNode); @@ -89,10 +108,10 @@ export class DocDisplayMetaService extends Service { return iconSet.BlockLinkIcon; } + // journal icon const journalDate = this._toDayjs( this.propertiesAdapter.getJournalPageDateString(docId) ); - if (journalDate) { if (!options?.compareDate) return iconSet.TodayIcon; const compareDate = dayjs(options?.compareDate); @@ -103,36 +122,65 @@ export class DocDisplayMetaService extends Service { : iconSet.TodayIcon; } - return options?.reference - ? finalMode === 'edgeless' + // reference icon + if (options?.reference) { + return finalMode === 'edgeless' ? iconSet.LinkedEdgelessIcon - : iconSet.LinkedPageIcon - : finalMode === 'edgeless' - ? iconSet.EdgelessIcon - : iconSet.PageIcon; + : iconSet.LinkedPageIcon; + } + + // emoji icon + const enableEmojiIcon = + get(this.featureFlagService.flags.enable_emoji_doc_icon.$) && + options?.enableEmojiIcon !== false; + if (enableEmojiIcon) { + const { emoji } = extractEmojiIcon(title); + if (emoji) return () => emoji; + } + + // default icon + return finalMode === 'edgeless' ? iconSet.EdgelessIcon : iconSet.PageIcon; }); } - title$(docId: string, originalTitle?: string) { + title$(docId: string, options?: DocDisplayTitleOptions) { return LiveData.computed(get => { const doc = get(this.docsService.list.doc$(docId)); const docTitle = doc ? get(doc.title$) : undefined; const journalDateString = this.propertiesAdapter.getJournalPageDateString(docId); - return journalDateString - ? i18nTime(journalDateString, { absolute: { accuracy: 'day' } }) - : originalTitle || - docTitle || - ({ - key: 'Untitled', - } as const); + + // journal + if (journalDateString) { + return i18nTime(journalDateString, { absolute: { accuracy: 'day' } }); + } + + if (options?.originalTitle) return options.originalTitle; + + // empty title + if (!docTitle) return { key: 'Untitled' } as const; + + // reference + if (options?.reference) return docTitle; + + // check emoji + const enableEmojiIcon = + get(this.featureFlagService.flags.enable_emoji_doc_icon.$) && + options?.enableEmojiIcon !== false; + if (enableEmojiIcon) { + const { rest } = extractEmojiIcon(docTitle); + return rest; + } + + // default + return docTitle; }); } getDocDisplayMeta(docRecord: DocRecord, originalTitle?: string) { return { - title: this.title$(docRecord.id, originalTitle).value, + title: this.title$(docRecord.id, { originalTitle }).value, icon: this.icon$(docRecord.id).value, updatedDate: docRecord.meta$.value.updatedDate, }; diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx index 6e15bf32cc..196d0ee46d 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx @@ -14,6 +14,7 @@ import { useI18n } from '@affine/i18n'; import { track } from '@affine/track'; import { DocsService, + FeatureFlagService, GlobalContextService, LiveData, useLiveData, @@ -47,11 +48,13 @@ export const ExplorerDocNode = ({ docsService, globalContextService, docDisplayMetaService, + featureFlagService, } = useServices({ DocsSearchService, DocsService, GlobalContextService, DocDisplayMetaService, + FeatureFlagService, }); // const pageInfoAdapter = useCurrentWorkspacePropertiesAdapter(); @@ -67,6 +70,9 @@ export const ExplorerDocNode = ({ ); const docTitle = useLiveData(docDisplayMetaService.title$(docId)); const isInTrash = useLiveData(docRecord?.trash$); + const enableEmojiIcon = useLiveData( + featureFlagService.flags.enable_emoji_doc_icon.$ + ); const Icon = useCallback( ({ className }: { className?: string }) => { @@ -206,6 +212,7 @@ export const ExplorerDocNode = ({ dndData={dndData} onDrop={handleDropOnDoc} renameable + extractEmojiAsIcon={enableEmojiIcon} collapsed={collapsed} setCollapsed={setCollapsed} canDrop={handleCanDrop}