add hidden groups and most frequently used

This commit is contained in:
ChaosExAnima 2025-11-19 15:55:44 +01:00
parent 0b81c547c2
commit d121dcba96
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
4 changed files with 225 additions and 38 deletions

View File

@ -1,9 +1,27 @@
import { createContext, useContext } from 'react';
import type { GroupKey, GroupMessage, SkinToneKey } from 'emojibase';
import groupData from 'emojibase-data/meta/groups.json';
import type { CustomEmojiData } from '@/mastodon/features/emoji/types';
type CustomGroupMessage = Omit<GroupMessage, 'key'> & {
export interface PickerContext {
skinTone: SkinTone;
hiddenGroups: string[];
recentlyUsed: string[];
}
const pickerContext = createContext<PickerContext>({
skinTone: 'default',
hiddenGroups: [],
recentlyUsed: [],
});
export const PickerContextProvider = pickerContext.Provider;
export const usePickerContext = () => useContext(pickerContext);
export type CustomGroupMessage = Omit<GroupMessage, 'key'> & {
key: string;
};

View File

@ -1,14 +1,19 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { FC } from 'react';
import { IconButton } from '@/mastodon/components/icon_button';
import type { AnyEmojiData } from '@/mastodon/features/emoji/types';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { IconButton } from '../../icon_button';
import { CustomEmojiProvider } from '../context';
import type { SkinTone } from './constants';
import { mockCustomEmojis, mockCustomGroups } from './constants';
import {
mockCustomEmojis,
mockCustomGroups,
PickerContextProvider,
usePickerContext,
} from './constants';
import { PickerGroupButton } from './group-button';
import { useLocaleMessages } from './hooks';
import { PickerGroupList } from './list';
@ -30,21 +35,60 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({
setShowSettings((prev) => !prev);
}, []);
const [skinTone, setSkinTone] = useState<SkinTone>('default');
const handleSkinToneChange = useCallback(
(tone: SkinTone) => {
setSkinTone(tone);
onSkinToneChange?.(tone);
},
[onSkinToneChange],
);
const [recentlyUsed, setRecentlyUsed] = useState<string[]>([
':blobcat_heart:',
':mastodon:',
'👍',
]);
const handleClearRecentlyUsed = useCallback(() => {
setRecentlyUsed([]);
}, []);
const handleEmojiPick = useCallback(
(emojiCode: string) => {
onSelect?.(emojiCode);
if (!recentlyUsed.includes(emojiCode)) {
setRecentlyUsed((prev) => [emojiCode, ...prev].slice(0, 10));
}
},
[onSelect, recentlyUsed],
);
const [hiddenGroups, setHiddenGroups] = useState<string[]>([]);
const handleToggleHiddenGroup = useCallback((group: string) => {
setHiddenGroups((prev) =>
prev.includes(group) ? prev.filter((g) => g !== group) : [...prev, group],
);
}, []);
return (
<CustomEmojiProvider emojis={mockCustomEmojis}>
<PickerContextProvider value={{ skinTone, hiddenGroups, recentlyUsed }}>
<div className={classes.wrapper}>
{showSettings ? (
<PickerSettings
onClose={handleSettingsClick}
onSkinToneChange={onSkinToneChange}
onSkinToneChange={handleSkinToneChange}
onToggleHiddenGroup={handleToggleHiddenGroup}
onClearRecentlyUsed={handleClearRecentlyUsed}
/>
) : (
<PickerMain
onSelect={onSelect}
onSelect={handleEmojiPick}
onSettingsClick={handleSettingsClick}
/>
)}
</div>
</PickerContextProvider>
</CustomEmojiProvider>
);
};
@ -90,6 +134,13 @@ const PickerMain: FC<
searchInput.focus();
}, []);
const { hiddenGroups } = usePickerContext();
const customGroups = useMemo(
() => mockCustomGroups.filter(({ key }) => !hiddenGroups.includes(key)),
[hiddenGroups],
);
return (
<>
<div className={classes.header}>
@ -108,7 +159,7 @@ const PickerMain: FC<
</div>
<div className={classes.main} ref={wrapperRef}>
{mockCustomGroups.map((group) => (
{customGroups.map((group) => (
<PickerGroupList
key={group.key}
group={group.key}
@ -126,7 +177,7 @@ const PickerMain: FC<
))}
</div>
<ul className={classes.nav}>
{mockCustomGroups.map((group) => (
{customGroups.map((group) => (
<PickerGroupButton
key={group.key}
onSelect={handleGroupSelect}
@ -134,7 +185,9 @@ const PickerMain: FC<
group={group.key}
/>
))}
{customGroups.length > 0 && (
<li key='separator' className={classes.separator} />
)}
{groups.map((group) => (
<PickerGroupButton
key={group.key}

View File

@ -1,28 +1,43 @@
import { useCallback, useMemo, useState } from 'react';
import type { ChangeEventHandler, FC } from 'react';
import { IconButton } from '@/mastodon/components/icon_button';
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { IconButton } from '../../icon_button';
import { Emoji } from '..';
import type { SkinTone } from './constants';
import { toneToEmoji } from './constants';
import type { CustomGroupMessage, SkinTone } from './constants';
import {
mockCustomEmojis,
mockCustomGroups,
toneToEmoji,
usePickerContext,
} from './constants';
import { useLocaleMessages } from './hooks';
import classes from './styles.module.css';
interface PickerSettingsProps {
onClose: () => void;
onSkinToneChange?: (skinTone: SkinTone) => void;
onClearRecentlyUsed: () => void;
onToggleHiddenGroup: (group: string) => void;
}
export const PickerSettings: FC<PickerSettingsProps> = ({
onClose,
onSkinToneChange,
onToggleHiddenGroup,
onClearRecentlyUsed,
}) => {
const [editHidden, setEditHidden] = useState(false);
const handleEditHiddenClick = useCallback(() => {
setEditHidden((prev) => !prev);
}, []);
return (
<>
<div className={classes.header}>
<h2 className={classes.headerTitle}>Emoji Picker Settings</h2>
<h2>Emoji Picker Settings</h2>
<IconButton
icon='close'
iconComponent={CloseIcon}
@ -31,10 +46,25 @@ export const PickerSettings: FC<PickerSettingsProps> = ({
/>
</div>
<div className={classes.settings}>
{!editHidden ? (
<>
<fieldset>
<legend>Skin tone</legend>
<SkinToneSelector onSkinToneChange={onSkinToneChange} />
</fieldset>
<button type='button' onClick={handleEditHiddenClick}>
Edit hidden groups
</button>
<button type='button' onClick={onClearRecentlyUsed}>
Reset recently used
</button>
</>
) : (
<HiddenGroupsSelector
onClose={handleEditHiddenClick}
onToggleHiddenGroup={onToggleHiddenGroup}
/>
)}
</div>
</>
);
@ -82,3 +112,57 @@ const SkinToneSelector: FC<Pick<PickerSettingsProps, 'onSkinToneChange'>> = ({
</div>
);
};
const HiddenGroupsSelector: FC<
Pick<PickerSettingsProps, 'onToggleHiddenGroup'> & { onClose: () => void }
> = ({ onClose, onToggleHiddenGroup }) => {
return (
<fieldset>
<legend className={classes.hiddenGroupHeader}>
<span>Uncheck to hide groups</span>
<button type='button' onClick={onClose}>
Go back
</button>
</legend>
<ul>
{mockCustomGroups.map((group) => (
<HiddenGroupItem
group={group}
key={group.key}
onToggleHiddenGroup={onToggleHiddenGroup}
/>
))}
</ul>
</fieldset>
);
};
const HiddenGroupItem: FC<
Pick<PickerSettingsProps, 'onToggleHiddenGroup'> & {
group: CustomGroupMessage;
}
> = ({ group, onToggleHiddenGroup }) => {
const groupEmoji = useMemo(
() => mockCustomEmojis.find((e) => e.category === group.key),
[group],
);
const handleToggle = useCallback(() => {
onToggleHiddenGroup(group.key);
}, [onToggleHiddenGroup, group.key]);
const { hiddenGroups } = usePickerContext();
return (
<li key={group.key}>
<label className={classes.hiddenGroupItem}>
<input
type='checkbox'
onChange={handleToggle}
checked={!hiddenGroups.includes(group.key)}
/>
<Emoji code={`:${groupEmoji?.shortcode}:`} />
{group.message}
</label>
</li>
);
};

View File

@ -25,14 +25,23 @@
gap: 0.5rem;
align-items: center;
grid-area: 1 / 1 / 1 / 3;
svg {
color: gray;
.search,
h2 {
flex-grow: 1;
}
h2 {
font-size: 1.25em;
}
button {
color: inherit;
}
}
.search {
flex-grow: 1;
appearance: none;
border: 1px solid gray;
border-radius: 0.25rem;
@ -163,17 +172,14 @@
}
}
.headerTitle {
flex-grow: 1;
color: gray;
font-size: 1.25em;
}
.skinTonesWrapper {
--max-button-size: 3rem;
display: flex;
gap: 0.5rem;
justify-content: space-between;
font-size: 1.5rem;
max-width: calc(var(--max-button-size) * 6 + 0.5rem * 5);
input {
appearance: none;
@ -188,7 +194,8 @@
border: 1px solid gray;
line-height: 100%;
cursor: pointer;
min-width: 2.5em;
width: 100%;
max-width: var(--max-button-size);
}
input:checked {
@ -203,3 +210,28 @@
line-height: 1;
}
}
.hiddenGroupHeader {
display: flex;
gap: 0.5rem;
width: 100%;
span {
flex-grow: 1;
}
button {
font-size: 0.75em;
}
}
.hiddenGroupItem {
display: flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
margin-bottom: 0.25rem;
background: lightgray;
border-radius: 0.25rem;
padding: 0.25rem;
}