feat(core): use emoji as folder icon (#7842)

![CleanShot 2024-08-12 at 22.33.42.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/g3jz87HxbjOJpXV3FPT7/c0c1a196-be29-4e45-a28d-87a55d80fc9d.png)
This commit is contained in:
EYHN
2024-08-14 10:35:22 +00:00
parent 0504d0b0ff
commit 50948318e0
9 changed files with 108 additions and 24 deletions

View File

@@ -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",

View File

@@ -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}

View File

@@ -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',

View File

@@ -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>

View File

@@ -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',
});
});

View 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,
};
}

View File

@@ -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';