sets up settings page

This commit is contained in:
ChaosExAnima 2025-11-13 16:20:07 +01:00
parent 0e7ac5425f
commit 7382171650
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
5 changed files with 178 additions and 98 deletions

View File

@ -0,0 +1,71 @@
import type { FC, MouseEventHandler } from 'react';
import { useCallback, useState, useEffect } from 'react';
import classNames from 'classnames';
import { loadUnicodeEmojiGroupIcon } from '@/mastodon/features/emoji/database';
import { useEmojiAppState } from '@/mastodon/features/emoji/mode';
import type { AnyEmojiData } from '@/mastodon/features/emoji/types';
import { Emoji } from '..';
import { mockCustomEmojis, groupKeysToNumber } from './constants';
import classes from './styles.module.css';
interface PickerGroupButtonProps {
onSelect: (key: string) => void;
group: string;
message: string;
disabled?: boolean;
}
export const PickerGroupButton: FC<PickerGroupButtonProps> = ({
onSelect,
message,
group,
disabled = false,
}) => {
const handleClick: MouseEventHandler = useCallback(
(event) => {
event.preventDefault();
onSelect(group);
},
[onSelect, group],
);
const { currentLocale } = useEmojiAppState();
const [icon, setIcon] = useState<AnyEmojiData | null>(() => {
const emoji = mockCustomEmojis.find((emoji) => emoji.category === group);
return emoji ?? null;
});
useEffect(() => {
if (icon !== null) {
return;
}
if (group in groupKeysToNumber) {
const groupNum = groupKeysToNumber[group];
if (typeof groupNum !== 'undefined') {
void loadUnicodeEmojiGroupIcon(groupNum, currentLocale).then(setIcon);
}
}
}, [currentLocale, group, icon]);
return (
<li>
<button
type='button'
title={message}
onClick={handleClick}
className={classNames(classes.groupButton)}
disabled={disabled}
>
{icon && (
<Emoji
code={'unicode' in icon ? icon.unicode : `:${icon.shortcode}:`}
/>
)}
</button>
</li>
);
};

View File

@ -0,0 +1,40 @@
import { useMemo, useState } from 'react';
import { usePrevious } from '@dnd-kit/utilities';
import type { MessagesDataset } from 'emojibase';
import enMessages from 'emojibase-data/en/messages.json';
import { useEmojiAppState } from '@/mastodon/features/emoji/mode';
import { groupsToHide } from './constants';
export function useLocaleMessages() {
const { currentLocale } = useEmojiAppState();
// This isn't needed in real life, as the current locale is only set on page load.
// However it Storybook can update the locale without a refresh.
const prevLocale = usePrevious(currentLocale);
const [messages, setMessages] = useState<MessagesDataset>(enMessages);
if (prevLocale !== currentLocale) {
// This is messy, but it's just for the mock picker.
import(
`../../../../../../node_modules/emojibase-data/${currentLocale}/messages.json`
)
.then((module: { default: MessagesDataset }) => {
setMessages(module.default);
})
.catch((err: unknown) => {
console.warn('fell back to en messages', err);
});
}
const groups = useMemo(
() => messages.groups.filter((group) => !groupsToHide.includes(group.key)),
[messages.groups],
);
return {
...messages,
groups,
};
}

View File

@ -1,26 +1,19 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import type { FC, MouseEventHandler } from 'react'; import type { FC, MouseEventHandler } from 'react';
import type { GroupMessage, MessagesDataset } from 'emojibase'; import classNames from 'classnames';
import messages from 'emojibase-data/en/messages.json';
import { loadUnicodeEmojiGroupIcon } from '@/mastodon/features/emoji/database';
import { useEmojiAppState } from '@/mastodon/features/emoji/mode';
import type { AnyEmojiData } from '@/mastodon/features/emoji/types'; import type { AnyEmojiData } from '@/mastodon/features/emoji/types';
import { usePrevious } from '@/mastodon/hooks/usePrevious';
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react'; import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
import { Emoji } from '..';
import { IconButton } from '../../icon_button'; import { IconButton } from '../../icon_button';
import { CustomEmojiProvider } from '../context'; import { CustomEmojiProvider } from '../context';
import { import { mockCustomEmojis, mockCustomGroups } from './constants';
groupKeysToNumber, import { PickerGroupButton } from './group-button';
groupsToHide, import { useLocaleMessages } from './hooks';
mockCustomEmojis,
mockCustomGroups,
} from './constants';
import { PickerGroupList } from './list'; import { PickerGroupList } from './list';
import { PickerSettings } from './settings';
import classes from './styles.module.css'; import classes from './styles.module.css';
interface MockEmojiPickerProps { interface MockEmojiPickerProps {
@ -39,6 +32,12 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({ onSelect }) => {
[onSelect], [onSelect],
); );
const [showSettings, setShowSettings] = useState(false);
const handleSettingsClick: MouseEventHandler = useCallback((event) => {
event.preventDefault();
setShowSettings((prev) => !prev);
}, []);
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const handleGroupSelect = useCallback((key: string) => { const handleGroupSelect = useCallback((key: string) => {
const wrapper = wrapperRef.current; const wrapper = wrapperRef.current;
@ -58,35 +57,7 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({ onSelect }) => {
} }
}, []); }, []);
const [showSettings, setShowSettings] = useState(false); const { groups } = useLocaleMessages();
const handleSettingsClick: MouseEventHandler = useCallback((event) => {
event.preventDefault();
setShowSettings((prev) => !prev);
}, []);
const { currentLocale } = useEmojiAppState();
// This isn't needed in real life, as the current locale is only set on page load.
const prevLocale = usePrevious(currentLocale);
const [groups, setGroups] = useState<GroupMessage[]>([]);
if (prevLocale !== currentLocale) {
// This is messy, but it's just for the mock picker.
import(
`../../../../../../node_modules/emojibase-data/${currentLocale}/messages.json`
)
.then((module: { default: MessagesDataset }) => {
setGroups(
module.default.groups.filter(
(group) => !groupsToHide.includes(group.key),
),
);
})
.catch((err: unknown) => {
console.warn('fell back to en messages', err);
setGroups(
messages.groups.filter((group) => !groupsToHide.includes(group.key)),
);
});
}
return ( return (
<CustomEmojiProvider emojis={mockCustomEmojis}> <CustomEmojiProvider emojis={mockCustomEmojis}>
@ -104,7 +75,7 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({ onSelect }) => {
onClick={handleSettingsClick} onClick={handleSettingsClick}
/> />
</div> </div>
{showSettings && <div className={classes.main}>Settings here</div>} {showSettings && <PickerSettings />}
{!showSettings && ( {!showSettings && (
<div className={classes.main} ref={wrapperRef}> <div className={classes.main} ref={wrapperRef}>
{mockCustomGroups.map((group) => ( {mockCustomGroups.map((group) => (
@ -125,22 +96,29 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({ onSelect }) => {
))} ))}
</div> </div>
)} )}
<ul className={classes.nav}> <ul
className={classNames(
classes.nav,
showSettings && classes.settingsNav,
)}
>
{mockCustomGroups.map((group) => ( {mockCustomGroups.map((group) => (
<PickerNavButton <PickerGroupButton
key={group.key} key={group.key}
onSelect={handleGroupSelect} onSelect={handleGroupSelect}
message={group.message} message={group.message}
group={group.key} group={group.key}
disabled={showSettings}
/> />
))} ))}
<li key='separator' className={classes.separator} /> <li key='separator' className={classes.separator} />
{groups.map((group) => ( {groups.map((group) => (
<PickerNavButton <PickerGroupButton
key={group.key} key={group.key}
onSelect={handleGroupSelect} onSelect={handleGroupSelect}
message={group.message} message={group.message}
group={group.key} group={group.key}
disabled={showSettings}
/> />
))} ))}
</ul> </ul>
@ -148,54 +126,3 @@ export const MockEmojiPicker: FC<MockEmojiPickerProps> = ({ onSelect }) => {
</CustomEmojiProvider> </CustomEmojiProvider>
); );
}; };
interface PickerNavProps {
onSelect: (key: string) => void;
group: string;
message: string;
}
const PickerNavButton: FC<PickerNavProps> = ({ onSelect, message, group }) => {
const handleClick: MouseEventHandler = useCallback(
(event) => {
event.preventDefault();
onSelect(group);
},
[onSelect, group],
);
const { currentLocale } = useEmojiAppState();
const [icon, setIcon] = useState<AnyEmojiData | null>(() => {
const emoji = mockCustomEmojis.find((emoji) => emoji.category === group);
return emoji ?? null;
});
useEffect(() => {
if (icon !== null) {
return;
}
if (group in groupKeysToNumber) {
const groupNum = groupKeysToNumber[group];
if (typeof groupNum !== 'undefined') {
void loadUnicodeEmojiGroupIcon(groupNum, currentLocale).then(setIcon);
}
}
}, [currentLocale, group, icon]);
return (
<li>
<button
type='button'
title={message}
onClick={handleClick}
className={classes.groupButton}
>
{icon && (
<Emoji
code={'unicode' in icon ? icon.unicode : `:${icon.shortcode}:`}
/>
)}
</button>
</li>
);
};

View File

@ -0,0 +1,36 @@
import type { FC } from 'react';
import type { SkinToneKey } from 'emojibase';
import { useLocaleMessages } from './hooks';
import classes from './styles.module.css';
const toneToEmoji: Record<SkinToneKey, string> = {
light: '👋🏻',
'medium-light': '👋🏼',
medium: '👋🏽',
'medium-dark': '👋🏾',
dark: '👋🏿',
};
export const PickerSettings: FC = () => {
const { skinTones } = useLocaleMessages();
return (
<div className={classes.main}>
<h1>Emoji Settings</h1>
<label>
<select>
<option value='default' title='Default skin tone'>
👋
</option>
{skinTones.map((tone) => (
<option key={tone.key} value={tone.key} title={tone.message}>
{toneToEmoji[tone.key]}
</option>
))}
</select>{' '}
Skin tone
</label>
</div>
);
};

View File

@ -1,5 +1,6 @@
.wrapper { .wrapper {
--max-width: 20rem; --max-width: 20rem;
--col-count: 6;
display: grid; display: grid;
gap: 0.5rem; gap: 0.5rem;
@ -92,7 +93,7 @@
.emojiGrid { .emojiGrid {
display: grid; display: grid;
grid-template-columns: repeat(6, 1fr); grid-template-columns: repeat(var(--col-count), 1fr);
gap: 0.5rem; gap: 0.5rem;
} }
@ -145,6 +146,11 @@
height: 2rem; height: 2rem;
border-radius: 50%; border-radius: 50%;
border: 1px solid gray; border: 1px solid gray;
.settingsNav & {
opacity: 0.5;
pointer-events: none;
}
} }
.separator { .separator {