2022-06-25 20:55:17 -07:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
2022-07-05 12:57:41 -07:00
|
|
|
import React, { useEffect, useState, useLayoutEffect } from 'react';
|
2022-06-29 01:25:57 -07:00
|
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
|
|
import { usePopper } from 'react-popper';
|
2022-06-25 20:55:17 -07:00
|
|
|
|
2022-06-29 01:25:57 -07:00
|
|
|
import { IconButton } from 'soapbox/components/ui';
|
|
|
|
import { useSettings } from 'soapbox/hooks';
|
2022-07-09 10:52:08 -07:00
|
|
|
import { isMobile } from 'soapbox/is_mobile';
|
2022-06-25 20:55:17 -07:00
|
|
|
|
2022-07-04 13:30:35 -07:00
|
|
|
import { buildCustomEmojis } from '../../emoji';
|
2022-06-25 20:55:17 -07:00
|
|
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
2022-07-04 13:30:35 -07:00
|
|
|
// import { Picker as EmojiPicker } from '../../emoji/emoji_picker';
|
2022-06-25 20:55:17 -07:00
|
|
|
|
2022-07-09 10:02:21 -07:00
|
|
|
import type { EmojiPick } from 'emoji-mart';
|
2022-06-29 01:25:57 -07:00
|
|
|
import type { List } from 'immutable';
|
2022-07-04 20:45:01 -07:00
|
|
|
import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji';
|
2022-06-29 01:25:57 -07:00
|
|
|
|
2022-06-25 20:55:17 -07:00
|
|
|
let EmojiPicker: any; // load asynchronously
|
|
|
|
|
2022-07-09 11:58:09 -07:00
|
|
|
// const categories = [
|
|
|
|
// 'frequent',
|
|
|
|
// 'custom',
|
|
|
|
// 'people',
|
|
|
|
// 'nature',
|
|
|
|
// 'foods',
|
|
|
|
// 'activity',
|
|
|
|
// 'places',
|
|
|
|
// 'objects',
|
|
|
|
// 'symbols',
|
|
|
|
// 'flags',
|
|
|
|
// ];
|
|
|
|
|
2022-06-25 20:55:17 -07:00
|
|
|
const messages = defineMessages({
|
|
|
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
2022-07-09 11:58:09 -07:00
|
|
|
emoji_pick: { id: 'emoji_button.pick', defaultMessage: 'Pick an emoji...' },
|
|
|
|
emoji_oh_no: { id: 'emoji_button.oh_no', defaultMessage: 'Oh no!' },
|
2022-06-25 20:55:17 -07:00
|
|
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
|
|
|
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emoji\'s found.' },
|
2022-07-09 11:58:09 -07:00
|
|
|
emoji_add_custom: { id: 'emoji_button.add_custom', defaultMessage: 'Add custom emoji' },
|
2022-06-25 20:55:17 -07:00
|
|
|
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' },
|
2022-07-09 11:58:09 -07:00
|
|
|
skins_choose: { id: 'emoji_button.skins_choose', defaultMessage: 'Choose default skin tone' },
|
|
|
|
skins_1: { id: 'emoji_button.skins_1', defaultMessage: 'Default' },
|
|
|
|
skins_2: { id: 'emoji_button.skins_2', defaultMessage: 'Light' },
|
|
|
|
skins_3: { id: 'emoji_button.skins_3', defaultMessage: 'Medium-Light' },
|
|
|
|
skins_4: { id: 'emoji_button.skins_4', defaultMessage: 'Medium' },
|
|
|
|
skins_5: { id: 'emoji_button.skins_5', defaultMessage: 'Medium-Dark' },
|
|
|
|
skins_6: { id: 'emoji_button.skins_6', defaultMessage: 'Dark' },
|
2022-06-25 20:55:17 -07:00
|
|
|
});
|
|
|
|
|
2022-07-09 11:58:09 -07:00
|
|
|
// TODO: fix types
|
2022-06-25 20:55:17 -07:00
|
|
|
interface IEmojiPickerDropdown {
|
2022-06-29 01:25:57 -07:00
|
|
|
custom_emojis: List<any>,
|
|
|
|
frequentlyUsedEmojis: string[],
|
2022-06-25 20:55:17 -07:00
|
|
|
intl: any,
|
2022-07-04 13:30:35 -07:00
|
|
|
onPickEmoji: (emoji: Emoji) => void,
|
2022-06-25 20:55:17 -07:00
|
|
|
onSkinTone: () => void,
|
2022-07-05 12:57:41 -07:00
|
|
|
condensed: boolean,
|
2022-07-10 02:06:37 -07:00
|
|
|
render: any,
|
2022-06-25 20:55:17 -07:00
|
|
|
}
|
|
|
|
|
2022-07-05 12:57:41 -07:00
|
|
|
// Fixes render bug where popover has a delayed position update
|
|
|
|
const RenderAfter = ({ children, update }: any) => {
|
|
|
|
const [nextTick, setNextTick] = useState(false);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setTimeout(() => {
|
|
|
|
setNextTick(true);
|
|
|
|
}, 0);
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
useLayoutEffect(() => {
|
|
|
|
if (nextTick) {
|
|
|
|
update();
|
|
|
|
}
|
|
|
|
}, [nextTick, update]);
|
|
|
|
|
|
|
|
return nextTick ? children : null;
|
|
|
|
};
|
|
|
|
|
2022-06-25 20:55:17 -07:00
|
|
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
|
|
|
|
2022-07-10 02:06:37 -07:00
|
|
|
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ custom_emojis, frequentlyUsedEmojis, onPickEmoji, onSkinTone, condensed, render: Render }) => {
|
2022-06-25 20:55:17 -07:00
|
|
|
const intl = useIntl();
|
|
|
|
const settings = useSettings();
|
|
|
|
const title = intl.formatMessage(messages.emoji);
|
|
|
|
const userTheme = settings.get('themeMode');
|
|
|
|
const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto';
|
|
|
|
|
|
|
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
2022-07-05 12:57:41 -07:00
|
|
|
const [popperReference, setPopperReference] = useState<HTMLButtonElement | null>(null);
|
2022-06-25 20:55:17 -07:00
|
|
|
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
|
|
|
|
|
|
|
|
const [visible, setVisible] = useState(false);
|
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
|
2022-07-09 10:52:08 -07:00
|
|
|
const placement = condensed ? 'bottom-start' : 'top-start';
|
2022-07-05 12:57:41 -07:00
|
|
|
const { styles, attributes, update } = usePopper(popperReference, popperElement, {
|
2022-07-09 10:52:08 -07:00
|
|
|
placement: isMobile(window.innerWidth) ? 'auto' : placement,
|
2022-06-25 20:55:17 -07:00
|
|
|
});
|
|
|
|
|
2022-07-10 04:42:22 -07:00
|
|
|
const handleToggle = (e: Event) => {
|
|
|
|
e.stopPropagation();
|
2022-06-25 20:55:17 -07:00
|
|
|
setVisible(!visible);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleDocClick = (e: any) => {
|
|
|
|
if (!containerElement?.contains(e.target) && !popperElement?.contains(e.target)) {
|
|
|
|
setVisible(false);
|
|
|
|
}
|
2022-06-29 01:25:57 -07:00
|
|
|
};
|
2022-06-25 20:55:17 -07:00
|
|
|
|
2022-07-04 20:45:01 -07:00
|
|
|
const handlePick = (emoji: EmojiPick) => {
|
2022-06-25 20:55:17 -07:00
|
|
|
setVisible(false);
|
2022-07-04 20:45:01 -07:00
|
|
|
|
|
|
|
if (emoji.native) {
|
|
|
|
onPickEmoji({
|
|
|
|
id: emoji.id,
|
|
|
|
colons: emoji.shortcodes,
|
|
|
|
custom: false,
|
|
|
|
native: emoji.native,
|
|
|
|
unified: emoji.unified,
|
|
|
|
} as NativeEmoji);
|
|
|
|
} else {
|
|
|
|
onPickEmoji({
|
|
|
|
id: emoji.id,
|
|
|
|
colons: emoji.shortcodes,
|
|
|
|
custom: true,
|
|
|
|
imageUrl: emoji.src,
|
|
|
|
} as CustomEmoji);
|
|
|
|
}
|
2022-06-29 01:25:57 -07:00
|
|
|
};
|
2022-06-25 20:55:17 -07:00
|
|
|
|
2022-07-09 11:58:09 -07:00
|
|
|
const getI18n = () => {
|
|
|
|
return {
|
|
|
|
search: intl.formatMessage(messages.emoji_search),
|
|
|
|
pick: intl.formatMessage(messages.emoji_pick),
|
|
|
|
search_no_results_1: intl.formatMessage(messages.emoji_oh_no),
|
|
|
|
search_no_results_2: intl.formatMessage(messages.emoji_not_found),
|
|
|
|
add_custom: intl.formatMessage(messages.emoji_add_custom),
|
|
|
|
categories: {
|
|
|
|
search: intl.formatMessage(messages.search_results),
|
|
|
|
frequent: 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),
|
|
|
|
},
|
|
|
|
skins: {
|
|
|
|
choose: intl.formatMessage(messages.skins_choose),
|
|
|
|
1: intl.formatMessage(messages.skins_1),
|
|
|
|
2: intl.formatMessage(messages.skins_2),
|
|
|
|
3: intl.formatMessage(messages.skins_3),
|
|
|
|
4: intl.formatMessage(messages.skins_4),
|
|
|
|
5: intl.formatMessage(messages.skins_5),
|
|
|
|
6: intl.formatMessage(messages.skins_6),
|
2022-07-09 12:16:42 -07:00
|
|
|
},
|
|
|
|
};
|
|
|
|
};
|
2022-07-09 10:52:08 -07:00
|
|
|
|
2022-06-25 20:55:17 -07:00
|
|
|
useEffect(() => {
|
|
|
|
document.addEventListener('click', handleDocClick, false);
|
|
|
|
document.addEventListener('touchend', handleDocClick, listenerOptions);
|
|
|
|
|
|
|
|
return function cleanup() {
|
2022-07-09 10:52:08 -07:00
|
|
|
document.removeEventListener('click', handleDocClick, false);
|
|
|
|
// @ts-ignore
|
|
|
|
document.removeEventListener('touchend', handleDocClick, listenerOptions);
|
2022-06-29 01:25:57 -07:00
|
|
|
};
|
2022-06-25 20:55:17 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
2022-07-10 04:42:22 -07:00
|
|
|
// fix scrolling focus issue
|
|
|
|
if (visible) {
|
|
|
|
document.body.style.overflow = "hidden";
|
|
|
|
} else {
|
|
|
|
document.body.style.overflow = "initial";
|
|
|
|
}
|
|
|
|
|
2022-06-25 20:55:17 -07:00
|
|
|
if (!EmojiPicker) {
|
|
|
|
setLoading(true);
|
|
|
|
|
|
|
|
EmojiPickerAsync().then(EmojiMart => {
|
|
|
|
EmojiPicker = EmojiMart.Picker;
|
|
|
|
|
|
|
|
setLoading(false);
|
|
|
|
}).catch(() => {
|
|
|
|
setLoading(false);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [visible]);
|
|
|
|
|
2022-07-09 10:52:08 -07:00
|
|
|
// TODO: move to class
|
2022-07-10 04:42:22 -07:00
|
|
|
const style: React.CSSProperties = !isMobile(window.innerWidth) ? styles.popper : {
|
2022-07-09 10:52:08 -07:00
|
|
|
...styles.popper, width: '100%',
|
|
|
|
};
|
2022-06-25 20:55:17 -07:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className='relative' ref={setContainerElement}>
|
2022-07-10 02:06:37 -07:00
|
|
|
<Render
|
|
|
|
handleToggle={handleToggle}
|
|
|
|
visible={visible}
|
|
|
|
loading={loading}
|
2022-06-25 20:55:17 -07:00
|
|
|
title={title}
|
2022-07-10 02:06:37 -07:00
|
|
|
setPopperReference={setPopperReference}
|
2022-06-25 20:55:17 -07:00
|
|
|
/>
|
|
|
|
|
|
|
|
{createPortal(
|
|
|
|
<div
|
2022-07-10 02:06:37 -07:00
|
|
|
className={classNames({ 'z-1000': true })}
|
2022-06-25 20:55:17 -07:00
|
|
|
ref={setPopperElement}
|
2022-07-09 10:52:08 -07:00
|
|
|
style={style}
|
2022-06-25 20:55:17 -07:00
|
|
|
{...attributes.popper}
|
|
|
|
>
|
2022-07-05 12:57:41 -07:00
|
|
|
{visible && (
|
|
|
|
<RenderAfter update={update}>
|
2022-07-09 10:52:08 -07:00
|
|
|
{!loading && (
|
|
|
|
<EmojiPicker
|
|
|
|
custom={[{ emojis: buildCustomEmojis(custom_emojis) }]}
|
|
|
|
title={title}
|
|
|
|
onEmojiSelect={handlePick}
|
|
|
|
recent={frequentlyUsedEmojis}
|
|
|
|
perLine={8}
|
|
|
|
skin={onSkinTone}
|
|
|
|
emojiSize={38}
|
|
|
|
emojiButtonSize={50}
|
|
|
|
set={'twitter'}
|
|
|
|
theme={theme}
|
2022-07-09 11:58:09 -07:00
|
|
|
i18n={getI18n()}
|
|
|
|
// categories={categories}
|
2022-07-09 10:52:08 -07:00
|
|
|
/>
|
|
|
|
)}
|
2022-07-05 12:57:41 -07:00
|
|
|
</RenderAfter>
|
|
|
|
)}
|
2022-06-25 20:55:17 -07:00
|
|
|
</div>,
|
2022-06-29 01:25:57 -07:00
|
|
|
document.body,
|
2022-06-25 20:55:17 -07:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default EmojiPickerDropdown;
|