mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): use emoji as folder icon (#7842)

This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
"fractional-indexing": "^3.2.0",
|
||||
"fractional-indexing-jittered": "^0.9.1",
|
||||
"fuse.js": "^7.0.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"graphql": "^16.8.1",
|
||||
"history": "^5.3.0",
|
||||
"idb": "^8.0.0",
|
||||
|
||||
@@ -34,7 +34,12 @@ import {
|
||||
RemoveFolderIcon,
|
||||
TagsIcon,
|
||||
} from '@blocksuite/icons/rc';
|
||||
import { DocsService, useLiveData, useServices } from '@toeverything/infra';
|
||||
import {
|
||||
DocsService,
|
||||
FeatureFlagService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
} from '@toeverything/infra';
|
||||
import { difference } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
@@ -65,7 +70,9 @@ export const ExplorerFolderNode = ({
|
||||
| NodeOperation[]
|
||||
| ((type: string, node: FolderNode) => NodeOperation[]);
|
||||
} & Omit<GenericExplorerNode, 'operations'>) => {
|
||||
const { organizeService } = useServices({ OrganizeService });
|
||||
const { organizeService } = useServices({
|
||||
OrganizeService,
|
||||
});
|
||||
const node = useLiveData(organizeService.folderTree.folderNode$(nodeId));
|
||||
const type = useLiveData(node?.type$);
|
||||
const data = useLiveData(node?.data$);
|
||||
@@ -180,15 +187,19 @@ export const ExplorerFolderNodeFolder = ({
|
||||
node: FolderNode;
|
||||
} & GenericExplorerNode) => {
|
||||
const t = useI18n();
|
||||
const { docsService, workbenchService } = useServices({
|
||||
const { docsService, workbenchService, featureFlagService } = useServices({
|
||||
DocsService,
|
||||
WorkbenchService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
FeatureFlagService,
|
||||
});
|
||||
const openDocsSelector = useSelectDoc();
|
||||
const openTagsSelector = useSelectTag();
|
||||
const openCollectionsSelector = useSelectCollection();
|
||||
const name = useLiveData(node.name$);
|
||||
const enableEmojiIcon = useLiveData(
|
||||
featureFlagService.flags.enable_emoji_folder_icon.$
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [newFolderId, setNewFolderId] = useState<string | null>(null);
|
||||
|
||||
@@ -780,6 +791,7 @@ export const ExplorerFolderNodeFolder = ({
|
||||
onDrop={handleDropOnFolder}
|
||||
defaultRenaming={defaultRenaming}
|
||||
renameable
|
||||
extractEmojiAsIcon={enableEmojiIcon}
|
||||
reorderable={reorderable}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={handleCollapsedChange}
|
||||
|
||||
@@ -72,6 +72,15 @@ export const icon = style({
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: '20px',
|
||||
});
|
||||
export const emojiIcon = style({
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
color: cssVarV2('icon/primary'),
|
||||
fontSize: cssVar('--affine-font-sm'),
|
||||
});
|
||||
export const collapsedIconContainer = style({
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
|
||||
@@ -14,6 +14,7 @@ import { RenameModal } from '@affine/component/rename-modal';
|
||||
import { appSidebarWidthAtom } from '@affine/core/components/app-sidebar/index.jotai';
|
||||
import { WorkbenchLink } from '@affine/core/modules/workbench';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { extractEmojiIcon } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import {
|
||||
ArrowDownSmallIcon,
|
||||
@@ -59,7 +60,7 @@ export type ExplorerTreeNodeIcon = React.ComponentType<{
|
||||
export const ExplorerTreeNode = ({
|
||||
children,
|
||||
icon: Icon,
|
||||
name,
|
||||
name: rawName,
|
||||
onClick,
|
||||
to,
|
||||
active,
|
||||
@@ -68,6 +69,7 @@ export const ExplorerTreeNode = ({
|
||||
onRename,
|
||||
disabled,
|
||||
collapsed,
|
||||
extractEmojiAsIcon,
|
||||
setCollapsed,
|
||||
canDrop,
|
||||
reorderable = true,
|
||||
@@ -87,6 +89,7 @@ export const ExplorerTreeNode = ({
|
||||
active?: boolean;
|
||||
reorderable?: boolean;
|
||||
defaultRenaming?: boolean;
|
||||
extractEmojiAsIcon?: boolean;
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
renameable?: boolean;
|
||||
@@ -117,6 +120,19 @@ export const ExplorerTreeNode = ({
|
||||
const [renaming, setRenaming] = useState(defaultRenaming);
|
||||
const [lastInGroup, setLastInGroup] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const { emoji, name } = useMemo(() => {
|
||||
if (!extractEmojiAsIcon || !rawName) {
|
||||
return {
|
||||
emoji: null,
|
||||
name: rawName,
|
||||
};
|
||||
}
|
||||
const { emoji, rest } = extractEmojiIcon(rawName);
|
||||
return {
|
||||
emoji,
|
||||
name: rest,
|
||||
};
|
||||
}, [extractEmojiAsIcon, rawName]);
|
||||
const { dragRef, dragging, CustomDragPreview } = useDraggable<
|
||||
AffineDNDData & { draggable: { __cid: string } }
|
||||
>(
|
||||
@@ -299,34 +315,38 @@ export const ExplorerTreeNode = ({
|
||||
data-active={active}
|
||||
data-disabled={disabled}
|
||||
>
|
||||
{Icon && (
|
||||
<div className={styles.iconsContainer}>
|
||||
<div
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="explorer-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
/>
|
||||
</div>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||
treeInstruction={treeInstruction}
|
||||
collapsed={collapsed}
|
||||
<div className={styles.iconsContainer}>
|
||||
<div
|
||||
data-disabled={disabled}
|
||||
onClick={handleCollapsedChange}
|
||||
data-testid="explorer-collapsed-button"
|
||||
className={styles.collapsedIconContainer}
|
||||
>
|
||||
<ArrowDownSmallIcon
|
||||
className={styles.collapsedIcon}
|
||||
data-collapsed={collapsed !== false}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{emoji ? (
|
||||
<div className={styles.emojiIcon}>{emoji}</div>
|
||||
) : (
|
||||
Icon && (
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
draggedOver={draggedOver && !isSelfDraggedOver}
|
||||
treeInstruction={treeInstruction}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{renameable && renaming && (
|
||||
<RenameModal
|
||||
open
|
||||
width={sidebarWidth - 32}
|
||||
onOpenChange={setRenaming}
|
||||
onRename={handleRename}
|
||||
currentName={name ?? ''}
|
||||
currentName={rawName ?? ''}
|
||||
>
|
||||
<div className={styles.itemRenameAnchor} />
|
||||
</RenameModal>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
import { extractEmojiIcon } from '../extract-emoji-icon';
|
||||
|
||||
test('extract-emoji-icon', () => {
|
||||
expect(extractEmojiIcon('👨🏻❤️💋👨🏻123')).toEqual({
|
||||
emoji: '👨🏻❤️💋👨🏻',
|
||||
rest: '123',
|
||||
});
|
||||
|
||||
expect(extractEmojiIcon('❤️123')).toEqual({
|
||||
emoji: null,
|
||||
rest: '❤️123',
|
||||
});
|
||||
});
|
||||
17
packages/frontend/core/src/utils/extract-emoji-icon.ts
Normal file
17
packages/frontend/core/src/utils/extract-emoji-icon.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import Graphemer from 'graphemer';
|
||||
|
||||
export function extractEmojiIcon(text: string) {
|
||||
const isStartsWithEmoji = /^(\p{Emoji_Presentation})/u.test(text);
|
||||
if (isStartsWithEmoji) {
|
||||
// emoji like "👨🏻❤️💋👨🏻" are combined. Graphemer can handle these.
|
||||
const emojiEnd = Graphemer.nextBreak(text, 0);
|
||||
return {
|
||||
emoji: text.substring(0, emojiEnd),
|
||||
rest: text.substring(emojiEnd),
|
||||
};
|
||||
}
|
||||
return {
|
||||
emoji: null,
|
||||
rest: text,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './create-emotion-cache';
|
||||
export * from './event';
|
||||
export * from './extract-emoji-icon';
|
||||
export * from './fractional-indexing';
|
||||
export * from './popup';
|
||||
export * from './string2color';
|
||||
|
||||
Reference in New Issue
Block a user