diff --git a/app/javascript/mastodon/components/emoji/picker/constants.ts b/app/javascript/mastodon/components/emoji/picker/constants.ts index de5eae8fe13..262464a82b2 100644 --- a/app/javascript/mastodon/components/emoji/picker/constants.ts +++ b/app/javascript/mastodon/components/emoji/picker/constants.ts @@ -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 & { +export interface PickerContext { + skinTone: SkinTone; + hiddenGroups: string[]; + recentlyUsed: string[]; +} + +const pickerContext = createContext({ + skinTone: 'default', + hiddenGroups: [], + recentlyUsed: [], +}); + +export const PickerContextProvider = pickerContext.Provider; + +export const usePickerContext = () => useContext(pickerContext); + +export type CustomGroupMessage = Omit & { key: string; }; diff --git a/app/javascript/mastodon/components/emoji/picker/index.tsx b/app/javascript/mastodon/components/emoji/picker/index.tsx index b5de756a2ef..e65df179e0f 100644 --- a/app/javascript/mastodon/components/emoji/picker/index.tsx +++ b/app/javascript/mastodon/components/emoji/picker/index.tsx @@ -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 = ({ setShowSettings((prev) => !prev); }, []); + const [skinTone, setSkinTone] = useState('default'); + const handleSkinToneChange = useCallback( + (tone: SkinTone) => { + setSkinTone(tone); + onSkinToneChange?.(tone); + }, + [onSkinToneChange], + ); + + const [recentlyUsed, setRecentlyUsed] = useState([ + ':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([]); + const handleToggleHiddenGroup = useCallback((group: string) => { + setHiddenGroups((prev) => + prev.includes(group) ? prev.filter((g) => g !== group) : [...prev, group], + ); + }, []); + return ( -
- {showSettings ? ( - - ) : ( - - )} -
+ +
+ {showSettings ? ( + + ) : ( + + )} +
+
); }; @@ -90,6 +134,13 @@ const PickerMain: FC< searchInput.focus(); }, []); + + const { hiddenGroups } = usePickerContext(); + const customGroups = useMemo( + () => mockCustomGroups.filter(({ key }) => !hiddenGroups.includes(key)), + [hiddenGroups], + ); + return ( <>
@@ -108,7 +159,7 @@ const PickerMain: FC<
- {mockCustomGroups.map((group) => ( + {customGroups.map((group) => (
    - {mockCustomGroups.map((group) => ( + {customGroups.map((group) => ( ))} -
  • + {customGroups.length > 0 && ( +
  • + )} {groups.map((group) => ( void; onSkinToneChange?: (skinTone: SkinTone) => void; + onClearRecentlyUsed: () => void; + onToggleHiddenGroup: (group: string) => void; } export const PickerSettings: FC = ({ onClose, onSkinToneChange, + onToggleHiddenGroup, + onClearRecentlyUsed, }) => { + const [editHidden, setEditHidden] = useState(false); + const handleEditHiddenClick = useCallback(() => { + setEditHidden((prev) => !prev); + }, []); + return ( <>
    -

    Emoji Picker Settings

    +

    Emoji Picker Settings

    = ({ />
    -
    - Skin tone - -
    + {!editHidden ? ( + <> +
    + Skin tone + +
    + + + + ) : ( + + )}
    ); @@ -82,3 +112,57 @@ const SkinToneSelector: FC> = ({
); }; + +const HiddenGroupsSelector: FC< + Pick & { onClose: () => void } +> = ({ onClose, onToggleHiddenGroup }) => { + return ( +
+ + Uncheck to hide groups + + +
    + {mockCustomGroups.map((group) => ( + + ))} +
+
+ ); +}; + +const HiddenGroupItem: FC< + Pick & { + 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 ( +
  • + +
  • + ); +}; diff --git a/app/javascript/mastodon/components/emoji/picker/styles.module.css b/app/javascript/mastodon/components/emoji/picker/styles.module.css index 645f57a2144..e63132accdf 100644 --- a/app/javascript/mastodon/components/emoji/picker/styles.module.css +++ b/app/javascript/mastodon/components/emoji/picker/styles.module.css @@ -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; +}