159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
import data from '@emoji-mart/data';
|
|
import Picker from '@emoji-mart/react';
|
|
import classNames from 'classnames';
|
|
import { List as ImmutableList } from 'immutable';
|
|
import React, { MouseEventHandler } from 'react';
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
|
|
import { IconButton } from 'soapbox/components/ui';
|
|
import { isMobile } from 'soapbox/is_mobile';
|
|
import { useTheme } from 'soapbox/hooks';
|
|
|
|
const messages = defineMessages({
|
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
|
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emoji\'s found.' },
|
|
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
|
|
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
|
|
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
|
people: { id: 'emoji_button.people', defaultMessage: 'People' },
|
|
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
|
|
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
|
|
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
|
|
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
|
|
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
|
|
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
|
|
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
|
|
skins: { id: 'emoji_button.skins', defaultMessage: 'Skins' },
|
|
});
|
|
|
|
interface IWrapper {
|
|
target: any,
|
|
show: boolean,
|
|
onClose: MouseEventHandler,
|
|
children: React.ReactNode,
|
|
}
|
|
|
|
const Wrapper: React.FC<IWrapper> = ({ target, show, onClose, children }) => {
|
|
if (!show) return null;
|
|
return (
|
|
<div className='emoji-picker fixed top-0 left-0 w-screen h-screen bg-gray-800 z-[100]'>
|
|
<div className='bg-white dark:bg-slate-800 flex flex-col overflow-hidden sm:rounded-lg absolute top-1/2 left-1/2 -translate-x-[50%] -translate-y-[50%] '>
|
|
<div className='p-1'>
|
|
<IconButton
|
|
className='ml-auto text-gray-500'
|
|
src={require('@tabler/icons/x.svg')}
|
|
onClick={onClose}
|
|
/>
|
|
</div>
|
|
{ children }
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface IEmojiPicker {
|
|
custom_emojis: ImmutableList<string>,
|
|
onPickEmoji: Function,
|
|
onSkinTone: Function,
|
|
skinTone: number,
|
|
button?: React.ReactNode,
|
|
}
|
|
|
|
const EmojiPickerUI : React.FC<IEmojiPicker> = ({
|
|
custom_emojis,
|
|
onPickEmoji,
|
|
}) => {
|
|
const root = React.useRef<HTMLDivElement>(null);
|
|
const [active, setActive] = React.useState(false);
|
|
const intl = useIntl();
|
|
const theme = useTheme();
|
|
|
|
const handleClose = React.useCallback((e = null) => {
|
|
if (e) {
|
|
e.stopPropagation();
|
|
}
|
|
setActive(false);
|
|
}, []);
|
|
|
|
const handleToggle = React.useCallback((e) => {
|
|
e.stopPropagation();
|
|
if (e.key === 'Escape') {
|
|
setActive(false);
|
|
return;
|
|
}
|
|
|
|
if ((!e.key || e.key === 'Enter')) {
|
|
setActive(!active);
|
|
}
|
|
}, [active]);
|
|
|
|
const buildCustomEmojis = React.useCallback((custom_emojis: ImmutableList<any>) => {
|
|
const emojis = custom_emojis.map((emoji) => (
|
|
{
|
|
id: emoji.get('shortcode'),
|
|
name: emoji.get('shortcode'),
|
|
keywords: [emoji.get('shortcode')],
|
|
skins: [{ src: emoji.get('static_url') }],
|
|
}
|
|
)).toJS();
|
|
return [{
|
|
id: 'custom',
|
|
name: intl.formatMessage(messages.custom),
|
|
emojis,
|
|
}];
|
|
}, []);
|
|
|
|
const handlePick = React.useCallback((emoji) => {
|
|
onPickEmoji({ ...emoji, native: emoji.native || emoji.shortcodes });
|
|
handleClose();
|
|
}, [handleClose, onPickEmoji]);
|
|
|
|
return (
|
|
<>
|
|
<div className='relative' ref={root} onKeyDown={handleToggle}>
|
|
<IconButton
|
|
className={classNames({
|
|
'text-gray-400 hover:text-gray-600': true,
|
|
})}
|
|
alt='😀'
|
|
src={require('@tabler/icons/mood-happy.svg')}
|
|
onClick={handleToggle}
|
|
/>
|
|
<Wrapper target={root} show={active} onClose={handleClose}>
|
|
<Picker
|
|
theme={theme}
|
|
dynamicWidth={isMobile(window.innerWidth)}
|
|
categories={['frequent', 'custom', 'people', 'nature', 'foods', 'activity', 'places', 'objects', 'symbols', 'flags']}
|
|
previewPosition='none'
|
|
custom={buildCustomEmojis(custom_emojis)}
|
|
data={data}
|
|
onEmojiSelect={handlePick}
|
|
i18n={
|
|
{
|
|
search: intl.formatMessage(messages.emoji_search),
|
|
notfound: intl.formatMessage(messages.emoji_not_found),
|
|
skins: intl.formatMessage(messages.skins),
|
|
categories: {
|
|
search: intl.formatMessage(messages.search_results),
|
|
recent: intl.formatMessage(messages.recent),
|
|
people: intl.formatMessage(messages.people),
|
|
nature: intl.formatMessage(messages.nature),
|
|
foods: intl.formatMessage(messages.food),
|
|
activity: intl.formatMessage(messages.activity),
|
|
places: intl.formatMessage(messages.travel),
|
|
objects: intl.formatMessage(messages.objects),
|
|
symbols: intl.formatMessage(messages.symbols),
|
|
flags: intl.formatMessage(messages.flags),
|
|
custom: intl.formatMessage(messages.custom),
|
|
},
|
|
}
|
|
}
|
|
/>
|
|
</Wrapper>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default EmojiPickerUI; |