kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
add emoji-picker
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>environments/review-compose-fe5g2p/deployments/978
rodzic
87877a3f96
commit
2b6d06ce01
|
@ -0,0 +1,210 @@
|
||||||
|
import classNames from 'clsx';
|
||||||
|
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
// @ts-ignore
|
||||||
|
import Overlay from 'react-overlays/lib/Overlay';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { useEmoji } from 'soapbox/actions/emojis';
|
||||||
|
import { getSettings, changeSetting } from 'soapbox/actions/settings';
|
||||||
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
|
import { EmojiPicker as EmojiPickerAsync } from 'soapbox/features/ui/util/async-components';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import EmojiPickerMenu from './emoji-picker-menu';
|
||||||
|
|
||||||
|
import type { Emoji as EmojiType } from 'soapbox/components/autosuggest_emoji';
|
||||||
|
import type { RootState } from 'soapbox/store';
|
||||||
|
|
||||||
|
let EmojiPicker: any, Emoji: any; // load asynchronously
|
||||||
|
|
||||||
|
const perLine = 8;
|
||||||
|
const lines = 2;
|
||||||
|
|
||||||
|
const DEFAULTS = [
|
||||||
|
'+1',
|
||||||
|
'grinning',
|
||||||
|
'kissing_heart',
|
||||||
|
'heart_eyes',
|
||||||
|
'laughing',
|
||||||
|
'stuck_out_tongue_winking_eye',
|
||||||
|
'sweat_smile',
|
||||||
|
'joy',
|
||||||
|
'yum',
|
||||||
|
'disappointed',
|
||||||
|
'thinking_face',
|
||||||
|
'weary',
|
||||||
|
'sob',
|
||||||
|
'sunglasses',
|
||||||
|
'heart',
|
||||||
|
'ok_hand',
|
||||||
|
];
|
||||||
|
|
||||||
|
const getFrequentlyUsedEmojis = createSelector([
|
||||||
|
(state: RootState) => state.settings.get('frequentlyUsedEmojis', ImmutableMap()),
|
||||||
|
], emojiCounters => {
|
||||||
|
let emojis = emojiCounters
|
||||||
|
.keySeq()
|
||||||
|
.sort((a: number, b: number) => emojiCounters.get(a) - emojiCounters.get(b))
|
||||||
|
.reverse()
|
||||||
|
.slice(0, perLine * lines)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
if (emojis.length < DEFAULTS.length) {
|
||||||
|
const uniqueDefaults = DEFAULTS.filter(emoji => !emojis.includes(emoji));
|
||||||
|
emojis = emojis.concat(uniqueDefaults.slice(0, DEFAULTS.length - emojis.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return emojis;
|
||||||
|
});
|
||||||
|
|
||||||
|
const getCustomEmojis = createSelector([
|
||||||
|
(state: RootState) => state.custom_emojis as ImmutableList<ImmutableMap<string, string>>,
|
||||||
|
], emojis => emojis.filter((e) => e.get('visible_in_picker')).sort((a, b) => {
|
||||||
|
const aShort = a.get('shortcode')!.toLowerCase();
|
||||||
|
const bShort = b.get('shortcode')!.toLowerCase();
|
||||||
|
|
||||||
|
if (aShort < bShort) {
|
||||||
|
return -1;
|
||||||
|
} else if (aShort > bShort) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}) as ImmutableList<ImmutableMap<string, string>>);
|
||||||
|
|
||||||
|
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' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IEmojiPickerDropdown {
|
||||||
|
onPickEmoji: (data: EmojiType) => void,
|
||||||
|
button?: JSX.Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ onPickEmoji, button }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const customEmojis = useAppSelector((state) => getCustomEmojis(state));
|
||||||
|
const skinTone = useAppSelector((state) => getSettings(state).get('skinTone') as number);
|
||||||
|
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
||||||
|
|
||||||
|
const [active, setActive] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [placement, setPlacement] = useState<'bottom' | 'top'>();
|
||||||
|
|
||||||
|
const target = useRef(null);
|
||||||
|
|
||||||
|
const onSkinTone = (skinTone: number) => {
|
||||||
|
dispatch(changeSetting(['skinTone'], skinTone));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePickEmoji = (emoji: EmojiType) => {
|
||||||
|
console.log(emoji);
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
dispatch(useEmoji(emoji));
|
||||||
|
|
||||||
|
if (onPickEmoji) {
|
||||||
|
onPickEmoji(emoji);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onShowDropdown: React.EventHandler<React.KeyboardEvent | React.MouseEvent> = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setActive(true);
|
||||||
|
|
||||||
|
if (!EmojiPicker) {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
EmojiPickerAsync().then(EmojiMart => {
|
||||||
|
EmojiPicker = EmojiMart.Picker;
|
||||||
|
Emoji = EmojiMart.Emoji;
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}).catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { top } = (e.target as any).getBoundingClientRect();
|
||||||
|
setPlacement(top * 2 < innerHeight ? 'bottom' : 'top');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHideDropdown = () => {
|
||||||
|
setActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggle: React.EventHandler<React.KeyboardEvent | React.MouseEvent> = (e) => {
|
||||||
|
if (!loading && (!(e as React.KeyboardEvent).key || (e as React.KeyboardEvent).key === 'Enter')) {
|
||||||
|
if (active) {
|
||||||
|
onHideDropdown();
|
||||||
|
} else {
|
||||||
|
onShowDropdown(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown: React.KeyboardEventHandler = e => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onHideDropdown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative' onKeyDown={handleKeyDown}>
|
||||||
|
<div
|
||||||
|
ref={target}
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
aria-expanded={active}
|
||||||
|
role='button'
|
||||||
|
onClick={onToggle}
|
||||||
|
onKeyDown={onToggle}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{button || <IconButton
|
||||||
|
className={classNames({
|
||||||
|
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||||
|
'pulse-loading': active && loading,
|
||||||
|
})}
|
||||||
|
title='😀'
|
||||||
|
src={require('@tabler/icons/mood-happy.svg')}
|
||||||
|
/>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Overlay show={active} placement={placement} target={target.current}>
|
||||||
|
<EmojiPickerMenu
|
||||||
|
customEmojis={customEmojis}
|
||||||
|
loading={loading}
|
||||||
|
onClose={onHideDropdown}
|
||||||
|
onPick={handlePickEmoji}
|
||||||
|
onSkinTone={onSkinTone}
|
||||||
|
skinTone={skinTone}
|
||||||
|
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { EmojiPicker, Emoji };
|
||||||
|
|
||||||
|
export default EmojiPickerDropdown;
|
|
@ -0,0 +1,170 @@
|
||||||
|
import classNames from 'clsx';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { buildCustomEmojis } from '../../../emoji/emoji';
|
||||||
|
|
||||||
|
import { EmojiPicker } from './emoji-picker-dropdown';
|
||||||
|
import ModifierPicker from './modifier-picker';
|
||||||
|
|
||||||
|
import type { Emoji } from 'soapbox/components/autosuggest_emoji';
|
||||||
|
|
||||||
|
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
const categoriesSort = [
|
||||||
|
'recent',
|
||||||
|
'custom',
|
||||||
|
'people',
|
||||||
|
'nature',
|
||||||
|
'foods',
|
||||||
|
'activity',
|
||||||
|
'places',
|
||||||
|
'objects',
|
||||||
|
'symbols',
|
||||||
|
'flags',
|
||||||
|
];
|
||||||
|
|
||||||
|
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' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IEmojiPickerMenu {
|
||||||
|
customEmojis: ImmutableList<ImmutableMap<string, string>>,
|
||||||
|
loading?: boolean,
|
||||||
|
onClose: () => void,
|
||||||
|
onPick: (emoji: Emoji) => void,
|
||||||
|
onSkinTone: (skinTone: number) => void,
|
||||||
|
skinTone?: number,
|
||||||
|
frequentlyUsedEmojis?: Array<string>,
|
||||||
|
style?: React.CSSProperties,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
|
||||||
|
customEmojis,
|
||||||
|
loading = true,
|
||||||
|
onClose,
|
||||||
|
onPick,
|
||||||
|
onSkinTone,
|
||||||
|
skinTone,
|
||||||
|
frequentlyUsedEmojis = [],
|
||||||
|
style = {},
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const [modifierOpen, setModifierOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDocumentClick = useCallback(e => {
|
||||||
|
if (node.current && !node.current.contains(e.target)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getI18n = () => {
|
||||||
|
return {
|
||||||
|
search: intl.formatMessage(messages.emoji_search),
|
||||||
|
notfound: intl.formatMessage(messages.emoji_not_found),
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (emoji: any) => {
|
||||||
|
if (!emoji.native) {
|
||||||
|
emoji.native = emoji.colons;
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
onPick(emoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModifierOpen = () => {
|
||||||
|
setModifierOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModifierClose = () => {
|
||||||
|
setModifierOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleModifierChange = (modifier: number) => {
|
||||||
|
onSkinTone(modifier);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', handleDocumentClick, false);
|
||||||
|
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleDocumentClick, false);
|
||||||
|
document.removeEventListener('touchend', handleDocumentClick, listenerOptions as any);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div style={{ width: 299 }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={node}>
|
||||||
|
<EmojiPicker
|
||||||
|
perLine={8}
|
||||||
|
emojiSize={22}
|
||||||
|
sheetSize={32}
|
||||||
|
custom={buildCustomEmojis(customEmojis)}
|
||||||
|
color=''
|
||||||
|
emoji=''
|
||||||
|
set='twitter'
|
||||||
|
title={title}
|
||||||
|
i18n={getI18n()}
|
||||||
|
onClick={handleClick}
|
||||||
|
include={categoriesSort}
|
||||||
|
recent={frequentlyUsedEmojis}
|
||||||
|
skin={skinTone}
|
||||||
|
showPreview={false}
|
||||||
|
backgroundImageFn={backgroundImageFn}
|
||||||
|
autoFocus
|
||||||
|
emojiTooltip
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModifierPicker
|
||||||
|
active={modifierOpen}
|
||||||
|
modifier={skinTone}
|
||||||
|
onOpen={handleModifierOpen}
|
||||||
|
onClose={handleModifierClose}
|
||||||
|
onChange={handleModifierChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiPickerMenu;
|
|
@ -0,0 +1,73 @@
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { Emoji } from './emoji-picker-dropdown';
|
||||||
|
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
|
||||||
|
|
||||||
|
interface IModifierPickerMenu {
|
||||||
|
active: boolean,
|
||||||
|
onSelect: (modifier: number) => void,
|
||||||
|
onClose: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModifierPickerMenu: React.FC<IModifierPickerMenu> = ({ active, onSelect, onClose }) => {
|
||||||
|
const node = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleClick: React.MouseEventHandler<HTMLButtonElement> = e => {
|
||||||
|
onSelect(+e.currentTarget.getAttribute('data-index')! * 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDocumentClick = useCallback((e => {
|
||||||
|
if (node.current && !node.current.contains(e.target)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
const attachListeners = () => {
|
||||||
|
document.addEventListener('click', handleDocumentClick, false);
|
||||||
|
document.addEventListener('touchend', handleDocumentClick, listenerOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeListeners = () => {
|
||||||
|
document.removeEventListener('click', handleDocumentClick, false);
|
||||||
|
document.removeEventListener('touchend', handleDocumentClick, listenerOptions as any);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
removeListeners();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active) attachListeners();
|
||||||
|
else removeListeners();
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={node}>
|
||||||
|
<button onClick={handleClick} data-index={1}>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleClick} data-index={2}>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleClick} data-index={3}>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleClick} data-index={4}>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleClick} data-index={5}>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} />
|
||||||
|
</button>
|
||||||
|
<button onClick={handleClick} data-index={6}>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModifierPickerMenu;
|
|
@ -0,0 +1,38 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Emoji } from './emoji-picker-dropdown';
|
||||||
|
import ModifierPickerMenu from './modifier-picker-menu';
|
||||||
|
|
||||||
|
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
|
||||||
|
|
||||||
|
interface IModifierPicker {
|
||||||
|
active: boolean,
|
||||||
|
modifier?: number,
|
||||||
|
onOpen: () => void,
|
||||||
|
onClose: () => void,
|
||||||
|
onChange: (skinTone: number) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModifierPicker: React.FC<IModifierPicker> = ({ active, modifier, onOpen, onClose, onChange }) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
if (active) {
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
onOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelect = (modifier: number) => {
|
||||||
|
onChange(modifier);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='emoji-picker-dropdown__modifiers'>
|
||||||
|
<Emoji emoji='thumbsup' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={handleClick} backgroundImageFn={backgroundImageFn} />
|
||||||
|
<ModifierPickerMenu active={active} onSelect={handleSelect} onClose={onClose} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModifierPicker;
|
Ładowanie…
Reference in New Issue