mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-29 02:50:46 +00:00
add hidden groups and most frequently used
This commit is contained in:
parent
0b81c547c2
commit
d121dcba96
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
<div className={classes.wrapper}>
|
||||
{showSettings ? (
|
||||
<PickerSettings
|
||||
onClose={handleSettingsClick}
|
||||
onSkinToneChange={onSkinToneChange}
|
||||
/>
|
||||
) : (
|
||||
<PickerMain
|
||||
onSelect={onSelect}
|
||||
onSettingsClick={handleSettingsClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<PickerContextProvider value={{ skinTone, hiddenGroups, recentlyUsed }}>
|
||||
<div className={classes.wrapper}>
|
||||
{showSettings ? (
|
||||
<PickerSettings
|
||||
onClose={handleSettingsClick}
|
||||
onSkinToneChange={handleSkinToneChange}
|
||||
onToggleHiddenGroup={handleToggleHiddenGroup}
|
||||
onClearRecentlyUsed={handleClearRecentlyUsed}
|
||||
/>
|
||||
) : (
|
||||
<PickerMain
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
<li key='separator' className={classes.separator} />
|
||||
{customGroups.length > 0 && (
|
||||
<li key='separator' className={classes.separator} />
|
||||
)}
|
||||
{groups.map((group) => (
|
||||
<PickerGroupButton
|
||||
key={group.key}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
<fieldset>
|
||||
<legend>Skin tone</legend>
|
||||
<SkinToneSelector onSkinToneChange={onSkinToneChange} />
|
||||
</fieldset>
|
||||
{!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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -25,14 +25,23 @@
|
|||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
grid-area: 1 / 1 / 1 / 3;
|
||||
color: gray;
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user