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 type { GroupKey, GroupMessage, SkinToneKey } from 'emojibase';
import groupData from 'emojibase-data/meta/groups.json'; import groupData from 'emojibase-data/meta/groups.json';
import type { CustomEmojiData } from '@/mastodon/features/emoji/types'; 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; 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 type { FC } from 'react';
import { IconButton } from '@/mastodon/components/icon_button';
import type { AnyEmojiData } from '@/mastodon/features/emoji/types'; import type { AnyEmojiData } from '@/mastodon/features/emoji/types';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { IconButton } from '../../icon_button';
import { CustomEmojiProvider } from '../context'; import { CustomEmojiProvider } from '../context';
import type { SkinTone } from './constants'; import type { SkinTone } from './constants';
import { mockCustomEmojis, mockCustomGroups } from './constants'; import {
mockCustomEmojis,
mockCustomGroups,
PickerContextProvider,
usePickerContext,
} from './constants';
import { PickerGroupButton } from './group-button'; import { PickerGroupButton } from './group-button';
import { useLocaleMessages } from './hooks'; import { useLocaleMessages } from './hooks';
import { PickerGroupList } from './list'; import { PickerGroupList } from './list';
@ -30,21 +35,60 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({
setShowSettings((prev) => !prev); 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 ( return (
<CustomEmojiProvider emojis={mockCustomEmojis}> <CustomEmojiProvider emojis={mockCustomEmojis}>
<div className={classes.wrapper}> <PickerContextProvider value={{ skinTone, hiddenGroups, recentlyUsed }}>
{showSettings ? ( <div className={classes.wrapper}>
<PickerSettings {showSettings ? (
onClose={handleSettingsClick} <PickerSettings
onSkinToneChange={onSkinToneChange} onClose={handleSettingsClick}
/> onSkinToneChange={handleSkinToneChange}
) : ( onToggleHiddenGroup={handleToggleHiddenGroup}
<PickerMain onClearRecentlyUsed={handleClearRecentlyUsed}
onSelect={onSelect} />
onSettingsClick={handleSettingsClick} ) : (
/> <PickerMain
)} onSelect={handleEmojiPick}
</div> onSettingsClick={handleSettingsClick}
/>
)}
</div>
</PickerContextProvider>
</CustomEmojiProvider> </CustomEmojiProvider>
); );
}; };
@ -90,6 +134,13 @@ const PickerMain: FC<
searchInput.focus(); searchInput.focus();
}, []); }, []);
const { hiddenGroups } = usePickerContext();
const customGroups = useMemo(
() => mockCustomGroups.filter(({ key }) => !hiddenGroups.includes(key)),
[hiddenGroups],
);
return ( return (
<> <>
<div className={classes.header}> <div className={classes.header}>
@ -108,7 +159,7 @@ const PickerMain: FC<
</div> </div>
<div className={classes.main} ref={wrapperRef}> <div className={classes.main} ref={wrapperRef}>
{mockCustomGroups.map((group) => ( {customGroups.map((group) => (
<PickerGroupList <PickerGroupList
key={group.key} key={group.key}
group={group.key} group={group.key}
@ -126,7 +177,7 @@ const PickerMain: FC<
))} ))}
</div> </div>
<ul className={classes.nav}> <ul className={classes.nav}>
{mockCustomGroups.map((group) => ( {customGroups.map((group) => (
<PickerGroupButton <PickerGroupButton
key={group.key} key={group.key}
onSelect={handleGroupSelect} onSelect={handleGroupSelect}
@ -134,7 +185,9 @@ const PickerMain: FC<
group={group.key} group={group.key}
/> />
))} ))}
<li key='separator' className={classes.separator} /> {customGroups.length > 0 && (
<li key='separator' className={classes.separator} />
)}
{groups.map((group) => ( {groups.map((group) => (
<PickerGroupButton <PickerGroupButton
key={group.key} key={group.key}

View File

@ -1,28 +1,43 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import type { ChangeEventHandler, FC } 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 CloseIcon from '@/material-icons/400-24px/close.svg?react';
import { IconButton } from '../../icon_button'; import { Emoji } from '..';
import type { SkinTone } from './constants'; import type { CustomGroupMessage, SkinTone } from './constants';
import { toneToEmoji } from './constants'; import {
mockCustomEmojis,
mockCustomGroups,
toneToEmoji,
usePickerContext,
} from './constants';
import { useLocaleMessages } from './hooks'; import { useLocaleMessages } from './hooks';
import classes from './styles.module.css'; import classes from './styles.module.css';
interface PickerSettingsProps { interface PickerSettingsProps {
onClose: () => void; onClose: () => void;
onSkinToneChange?: (skinTone: SkinTone) => void; onSkinToneChange?: (skinTone: SkinTone) => void;
onClearRecentlyUsed: () => void;
onToggleHiddenGroup: (group: string) => void;
} }
export const PickerSettings: FC<PickerSettingsProps> = ({ export const PickerSettings: FC<PickerSettingsProps> = ({
onClose, onClose,
onSkinToneChange, onSkinToneChange,
onToggleHiddenGroup,
onClearRecentlyUsed,
}) => { }) => {
const [editHidden, setEditHidden] = useState(false);
const handleEditHiddenClick = useCallback(() => {
setEditHidden((prev) => !prev);
}, []);
return ( return (
<> <>
<div className={classes.header}> <div className={classes.header}>
<h2 className={classes.headerTitle}>Emoji Picker Settings</h2> <h2>Emoji Picker Settings</h2>
<IconButton <IconButton
icon='close' icon='close'
iconComponent={CloseIcon} iconComponent={CloseIcon}
@ -31,10 +46,25 @@ export const PickerSettings: FC<PickerSettingsProps> = ({
/> />
</div> </div>
<div className={classes.settings}> <div className={classes.settings}>
<fieldset> {!editHidden ? (
<legend>Skin tone</legend> <>
<SkinToneSelector onSkinToneChange={onSkinToneChange} /> <fieldset>
</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> </div>
</> </>
); );
@ -82,3 +112,57 @@ const SkinToneSelector: FC<Pick<PickerSettingsProps, 'onSkinToneChange'>> = ({
</div> </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; gap: 0.5rem;
align-items: center; align-items: center;
grid-area: 1 / 1 / 1 / 3; grid-area: 1 / 1 / 1 / 3;
color: gray;
svg { .search,
color: gray; h2 {
flex-grow: 1;
}
h2 {
font-size: 1.25em;
}
button {
color: inherit;
} }
} }
.search { .search {
flex-grow: 1;
appearance: none; appearance: none;
border: 1px solid gray; border: 1px solid gray;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -163,17 +172,14 @@
} }
} }
.headerTitle {
flex-grow: 1;
color: gray;
font-size: 1.25em;
}
.skinTonesWrapper { .skinTonesWrapper {
--max-button-size: 3rem;
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
justify-content: space-between; justify-content: space-between;
font-size: 1.5rem; font-size: 1.5rem;
max-width: calc(var(--max-button-size) * 6 + 0.5rem * 5);
input { input {
appearance: none; appearance: none;
@ -188,7 +194,8 @@
border: 1px solid gray; border: 1px solid gray;
line-height: 100%; line-height: 100%;
cursor: pointer; cursor: pointer;
min-width: 2.5em; width: 100%;
max-width: var(--max-button-size);
} }
input:checked { input:checked {
@ -203,3 +210,28 @@
line-height: 1; 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;
}