mirror of
https://github.com/mastodon/mastodon.git
synced 2025-11-29 19:03:41 +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 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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user