Merge branch 'update-emoji-mart-2' into 'develop'
Update emoji mart See merge request soapbox-pub/soapbox!2309
This commit is contained in:
commit
b54a466bfd
|
@ -4,7 +4,8 @@ import throttle from 'lodash/throttle';
|
||||||
import { defineMessages, IntlShape } from 'react-intl';
|
import { defineMessages, IntlShape } from 'react-intl';
|
||||||
|
|
||||||
import api from 'soapbox/api';
|
import api from 'soapbox/api';
|
||||||
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
|
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||||
|
import emojiSearch from 'soapbox/features/emoji/search';
|
||||||
import { tagHistory } from 'soapbox/settings';
|
import { tagHistory } from 'soapbox/settings';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
@ -19,8 +20,8 @@ import { openModal, closeModal } from './modals';
|
||||||
import { getSettings } from './settings';
|
import { getSettings } from './settings';
|
||||||
import { createStatus } from './statuses';
|
import { createStatus } from './statuses';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
|
||||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||||
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
import type { Account, APIEntity, Status, Tag } from 'soapbox/types/entities';
|
||||||
import type { History } from 'soapbox/types/history';
|
import type { History } from 'soapbox/types/history';
|
||||||
|
@ -516,7 +517,9 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, composeId,
|
||||||
}, 200, { leading: true, trailing: true });
|
}, 200, { leading: true, trailing: true });
|
||||||
|
|
||||||
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, getState: () => RootState, composeId: string, token: string) => {
|
||||||
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 } as any);
|
const state = getState();
|
||||||
|
const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }, state.custom_emojis);
|
||||||
|
|
||||||
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
dispatch(readyComposeSuggestionsEmojis(composeId, token, results));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -561,7 +564,7 @@ const selectComposeSuggestion = (composeId: string, position: number, token: str
|
||||||
let completion, startPosition;
|
let completion, startPosition;
|
||||||
|
|
||||||
if (typeof suggestion === 'object' && suggestion.id) {
|
if (typeof suggestion === 'object' && suggestion.id) {
|
||||||
completion = suggestion.native || suggestion.colons;
|
completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons;
|
||||||
startPosition = position - 1;
|
startPosition = position - 1;
|
||||||
|
|
||||||
dispatch(useEmoji(suggestion));
|
dispatch(useEmoji(suggestion));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { saveSettings } from './settings';
|
import { saveSettings } from './settings';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
import type { AppDispatch } from 'soapbox/store';
|
import type { AppDispatch } from 'soapbox/store';
|
||||||
|
|
||||||
const EMOJI_USE = 'EMOJI_USE';
|
const EMOJI_USE = 'EMOJI_USE';
|
||||||
|
|
|
@ -569,7 +569,7 @@ const rejectEventParticipationRequestFail = (id: string, accountId: string, erro
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchEventIcs = (id: string) =>
|
const fetchEventIcs = (id: string) =>
|
||||||
(dispatch: any, getState: () => RootState) =>
|
(dispatch: AppDispatch, getState: () => RootState) =>
|
||||||
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
|
||||||
|
|
||||||
const cancelEventCompose = () => ({
|
const cancelEventCompose = () => ({
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||||
|
|
||||||
import Emoji from './emoji';
|
import Emoji from './emoji';
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,13 @@ import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { TransitionMotion, spring } from 'react-motion';
|
||||||
|
|
||||||
import { Icon } from 'soapbox/components/ui';
|
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||||
import EmojiPickerDropdown from 'soapbox/features/compose/components/emoji-picker/emoji-picker-dropdown';
|
|
||||||
import { useSettings } from 'soapbox/hooks';
|
import { useSettings } from 'soapbox/hooks';
|
||||||
|
|
||||||
import Reaction from './reaction';
|
import Reaction from './reaction';
|
||||||
|
|
||||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||||
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
import type { AnnouncementReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IReactionsBar {
|
interface IReactionsBar {
|
||||||
|
@ -24,7 +23,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
||||||
const reduceMotion = useSettings().get('reduceMotion');
|
const reduceMotion = useSettings().get('reduceMotion');
|
||||||
|
|
||||||
const handleEmojiPick = (data: Emoji) => {
|
const handleEmojiPick = (data: Emoji) => {
|
||||||
addReaction(announcementId, data.native.replace(/:/g, ''));
|
addReaction(announcementId, (data as NativeEmoji).native.replace(/:/g, ''));
|
||||||
};
|
};
|
||||||
|
|
||||||
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
const willEnter = () => ({ scale: reduceMotion ? 1 : 0 });
|
||||||
|
@ -55,7 +54,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} button={<Icon className='h-4 w-4 text-gray-400 hover:text-gray-600 dark:hover:text-white' src={require('@tabler/icons/plus.svg')} />} />}
|
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</TransitionMotion>
|
</TransitionMotion>
|
||||||
|
|
|
@ -1,38 +1,30 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import unicodeMapping from 'soapbox/features/emoji/emoji-unicode-mapping-light';
|
import { isCustomEmoji } from 'soapbox/features/emoji';
|
||||||
|
import unicodeMapping from 'soapbox/features/emoji/mapping';
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
export type Emoji = {
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
id: string
|
|
||||||
custom: boolean
|
|
||||||
imageUrl: string
|
|
||||||
native: string
|
|
||||||
colons: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnicodeMapping = {
|
|
||||||
filename: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAutosuggestEmoji {
|
interface IAutosuggestEmoji {
|
||||||
emoji: Emoji
|
emoji: Emoji
|
||||||
}
|
}
|
||||||
|
|
||||||
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||||
let url;
|
let url, alt;
|
||||||
|
|
||||||
if (emoji.custom) {
|
if (isCustomEmoji(emoji)) {
|
||||||
url = emoji.imageUrl;
|
url = emoji.imageUrl;
|
||||||
|
alt = emoji.colons;
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
||||||
const mapping: UnicodeMapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')];
|
|
||||||
|
|
||||||
if (!mapping) {
|
if (!mapping) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
url = joinPublicPath(`packs/emoji/${mapping.filename}.svg`);
|
url = joinPublicPath(`packs/emoji/${mapping.unified}.svg`);
|
||||||
|
alt = emoji.native;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -40,7 +32,7 @@ const AutosuggestEmoji: React.FC<IAutosuggestEmoji> = ({ emoji }) => {
|
||||||
<img
|
<img
|
||||||
className='emojione'
|
className='emojione'
|
||||||
src={url}
|
src={url}
|
||||||
alt={emoji.native || emoji.colons}
|
alt={alt}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{emoji.colons}
|
{emoji.colons}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { List as ImmutableList } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import AutosuggestEmoji, { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import { Input, Portal } from 'soapbox/components/ui';
|
import { Input, Portal } from 'soapbox/components/ui';
|
||||||
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||||
|
@ -12,6 +12,7 @@ import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
import type { Menu, MenuItem } from 'soapbox/components/dropdown-menu';
|
||||||
import type { InputThemes } from 'soapbox/components/ui/input/input';
|
import type { InputThemes } from 'soapbox/components/ui/input/input';
|
||||||
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
export type AutoSuggestion = string | Emoji;
|
export type AutoSuggestion = string | Emoji;
|
||||||
|
|
||||||
|
|
|
@ -4,14 +4,14 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
|
|
||||||
import { Portal } from 'soapbox/components/ui';
|
import { Portal } from 'soapbox/components/ui';
|
||||||
|
import AutosuggestAccount from 'soapbox/features/compose/components/autosuggest-account';
|
||||||
|
import { isRtl } from 'soapbox/rtl';
|
||||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import AutosuggestAccount from '../features/compose/components/autosuggest-account';
|
import AutosuggestEmoji from './autosuggest-emoji';
|
||||||
import { isRtl } from '../rtl';
|
|
||||||
|
|
||||||
import AutosuggestEmoji, { Emoji } from './autosuggest-emoji';
|
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
interface IAutosuggesteTextarea {
|
interface IAutosuggesteTextarea {
|
||||||
id?: string
|
id?: string
|
||||||
|
|
|
@ -112,6 +112,7 @@ const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, chi
|
||||||
referenceElement={referenceElement}
|
referenceElement={referenceElement}
|
||||||
onReact={handleReact}
|
onReact={handleReact}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
|
onClose={() => setVisible(false)}
|
||||||
/>
|
/>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,10 +3,12 @@ import clsx from 'clsx';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { usePopper } from 'react-popper';
|
import { usePopper } from 'react-popper';
|
||||||
|
|
||||||
import { Emoji, HStack, IconButton } from 'soapbox/components/ui';
|
import { Emoji as EmojiComponent, HStack, IconButton } from 'soapbox/components/ui';
|
||||||
import { Picker } from 'soapbox/features/emoji/emoji-picker';
|
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown';
|
||||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
interface IEmojiButton {
|
interface IEmojiButton {
|
||||||
/** Unicode emoji character. */
|
/** Unicode emoji character. */
|
||||||
emoji: string
|
emoji: string
|
||||||
|
@ -29,7 +31,7 @@ const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabInd
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
||||||
<Emoji className='h-6 w-6 duration-100 hover:scale-110' emoji={emoji} />
|
<EmojiComponent className='h-6 w-6 duration-100 hover:scale-110' emoji={emoji} />
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -68,10 +70,16 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (referenceElement?.contains(event.target as Node) || popperElement?.contains(event.target as Node)) {
|
if ([referenceElement, popperElement, document.querySelector('em-emoji-picker')].some(el => el?.contains(event.target as Node))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.querySelector('em-emoji-picker')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
return setExpanded(false);
|
||||||
|
}
|
||||||
|
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
@ -93,6 +101,14 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
setExpanded(true);
|
setExpanded(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePickEmoji = (emoji: Emoji) => {
|
||||||
|
onReact((emoji as NativeEmoji).native);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
@ -103,7 +119,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
};
|
};
|
||||||
}, [referenceElement]);
|
}, [referenceElement, popperElement]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && update) {
|
if (visible && update) {
|
||||||
|
@ -117,6 +133,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
}
|
}
|
||||||
}, [expanded, update]);
|
}, [expanded, update]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx('z-[101] transition-opacity duration-100', {
|
className={clsx('z-[101] transition-opacity duration-100', {
|
||||||
|
@ -127,10 +144,12 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
>
|
>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<Picker
|
<EmojiPickerDropdown
|
||||||
set='twitter'
|
visible={expanded}
|
||||||
backgroundImageFn={() => require('emoji-datasource/img/twitter/sheets/32.png')}
|
setVisible={setExpanded}
|
||||||
onClick={(emoji: any) => onReact(emoji.native)}
|
update={update}
|
||||||
|
withCustom={false}
|
||||||
|
onPickEmoji={handlePickEmoji}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<HStack
|
<HStack
|
||||||
|
|
|
@ -103,7 +103,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, onOpenMedia }) => {
|
||||||
} else if (attachment.type === 'audio') {
|
} else if (attachment.type === 'audio') {
|
||||||
const remoteURL = attachment.remote_url || '';
|
const remoteURL = attachment.remote_url || '';
|
||||||
const fileExtensionLastIndex = remoteURL.lastIndexOf('.');
|
const fileExtensionLastIndex = remoteURL.lastIndexOf('.');
|
||||||
const fileExtension = remoteURL.substr(fileExtensionLastIndex + 1).toUpperCase();
|
const fileExtension = remoteURL.slice(fileExtensionLastIndex + 1).toUpperCase();
|
||||||
thumbnail = (
|
thumbnail = (
|
||||||
<div className='media-gallery__item-thumbnail'>
|
<div className='media-gallery__item-thumbnail'>
|
||||||
<span className='media-gallery__item__icons'><Icon src={require('@tabler/icons/volume.svg')} /></span>
|
<span className='media-gallery__item__icons'><Icon src={require('@tabler/icons/volume.svg')} /></span>
|
||||||
|
|
|
@ -6,13 +6,15 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import { Button, Combobox, ComboboxInput, ComboboxList, ComboboxOption, ComboboxPopover, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
import { Button, Combobox, ComboboxInput, ComboboxList, ComboboxOption, ComboboxPopover, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||||
import UploadButton from 'soapbox/features/compose/components/upload-button';
|
import UploadButton from 'soapbox/features/compose/components/upload-button';
|
||||||
import { search as emojiSearch } from 'soapbox/features/emoji/emoji-mart-search-light';
|
import emojiSearch from 'soapbox/features/emoji/search';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
import { Attachment } from 'soapbox/types/entities';
|
import { Attachment } from 'soapbox/types/entities';
|
||||||
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
||||||
|
|
||||||
import ChatTextarea from './chat-textarea';
|
import ChatTextarea from './chat-textarea';
|
||||||
|
|
||||||
|
import type { Emoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
placeholder: { id: 'chat.input.placeholder', defaultMessage: 'Type a message' },
|
placeholder: { id: 'chat.input.placeholder', defaultMessage: 'Type a message' },
|
||||||
send: { id: 'chat.actions.send', defaultMessage: 'Send' },
|
send: { id: 'chat.actions.send', defaultMessage: 'Send' },
|
||||||
|
@ -31,7 +33,7 @@ const initialSuggestionState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Suggestion {
|
interface Suggestion {
|
||||||
list: { native: string, colons: string }[]
|
list: Emoji[]
|
||||||
tokenStart: number
|
tokenStart: number
|
||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
@ -209,7 +211,7 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
|
||||||
key={emojiSuggestion.colons}
|
key={emojiSuggestion.colons}
|
||||||
value={renderSuggestionValue(emojiSuggestion)}
|
value={renderSuggestionValue(emojiSuggestion)}
|
||||||
>
|
>
|
||||||
<span>{emojiSuggestion.native}</span>
|
<span>{(emojiSuggestion as NativeEmoji).native}</span>
|
||||||
<span className='ml-1'>
|
<span className='ml-1'>
|
||||||
{emojiSuggestion.colons}
|
{emojiSuggestion.colons}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Text } from 'soapbox/components/ui';
|
import { Text } from 'soapbox/components/ui';
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { EmojiReaction } from 'soapbox/types/entities';
|
import { EmojiReaction } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IChatMessageReaction {
|
interface IChatMessageReaction {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { openModal } from 'soapbox/actions/modals';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport } from 'soapbox/actions/reports';
|
||||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import Bundle from 'soapbox/features/ui/components/bundle';
|
import Bundle from 'soapbox/features/ui/components/bundle';
|
||||||
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
import { MediaGallery } from 'soapbox/features/ui/util/async-components';
|
||||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||||
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
|
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
|
||||||
import { Button, HStack, Stack } from 'soapbox/components/ui';
|
import { Button, HStack, Stack } from 'soapbox/components/ui';
|
||||||
|
import EmojiPickerDropdown from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
|
||||||
import { isMobile } from 'soapbox/is-mobile';
|
import { isMobile } from 'soapbox/is-mobile';
|
||||||
|
|
||||||
|
@ -26,7 +27,6 @@ import UploadButtonContainer from '../containers/upload-button-container';
|
||||||
import WarningContainer from '../containers/warning-container';
|
import WarningContainer from '../containers/warning-container';
|
||||||
import { countableText } from '../util/counter';
|
import { countableText } from '../util/counter';
|
||||||
|
|
||||||
import EmojiPickerDropdown from './emoji-picker/emoji-picker-dropdown';
|
|
||||||
import MarkdownButton from './markdown-button';
|
import MarkdownButton from './markdown-button';
|
||||||
import PollButton from './poll-button';
|
import PollButton from './poll-button';
|
||||||
import PollForm from './polls/poll-form';
|
import PollForm from './polls/poll-form';
|
||||||
|
@ -40,7 +40,7 @@ import UploadForm from './upload-form';
|
||||||
import VisualCharacterCounter from './visual-character-counter';
|
import VisualCharacterCounter from './visual-character-counter';
|
||||||
import Warning from './warning';
|
import Warning from './warning';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
// FIXME: Make this less brittle
|
// FIXME: Make this less brittle
|
||||||
getClickableArea(),
|
getClickableArea(),
|
||||||
document.querySelector('.privacy-dropdown__dropdown'),
|
document.querySelector('.privacy-dropdown__dropdown'),
|
||||||
document.querySelector('.emoji-picker-dropdown__menu'),
|
document.querySelector('em-emoji-picker'),
|
||||||
document.getElementById('modal-overlay'),
|
document.getElementById('modal-overlay'),
|
||||||
].some(element => element?.contains(e.target as any));
|
].some(element => element?.contains(e.target as any));
|
||||||
};
|
};
|
||||||
|
@ -179,7 +179,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
|
|
||||||
const handleEmojiPick = (data: Emoji) => {
|
const handleEmojiPick = (data: Emoji) => {
|
||||||
const position = autosuggestTextareaRef.current!.textarea!.selectionStart;
|
const position = autosuggestTextareaRef.current!.textarea!.selectionStart;
|
||||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
const needsSpace = !!data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||||
|
|
||||||
dispatch(insertEmojiCompose(id, position, data, needsSpace));
|
dispatch(insertEmojiCompose(id, position, data, needsSpace));
|
||||||
};
|
};
|
||||||
|
@ -226,7 +226,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
const renderButtons = useCallback(() => (
|
const renderButtons = useCallback(() => (
|
||||||
<HStack alignItems='center' space={2}>
|
<HStack alignItems='center' space={2}>
|
||||||
{features.media && <UploadButtonContainer composeId={id} />}
|
{features.media && <UploadButtonContainer composeId={id} />}
|
||||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} condensed={shouldCondense} />
|
||||||
{features.polls && <PollButton composeId={id} />}
|
{features.polls && <PollButton composeId={id} />}
|
||||||
{features.privacyScopes && !group && !groupId && <PrivacyDropdown composeId={id} />}
|
{features.privacyScopes && !group && !groupId && <PrivacyDropdown composeId={id} />}
|
||||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||||
|
|
|
@ -1,209 +0,0 @@
|
||||||
import clsx 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) => {
|
|
||||||
// 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={clsx({
|
|
||||||
'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;
|
|
|
@ -1,171 +0,0 @@
|
||||||
import clsx 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, categoriesFromEmojis } 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 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 categoriesSort = [
|
|
||||||
'recent',
|
|
||||||
'people',
|
|
||||||
'nature',
|
|
||||||
'foods',
|
|
||||||
'activity',
|
|
||||||
'places',
|
|
||||||
'objects',
|
|
||||||
'symbols',
|
|
||||||
'flags',
|
|
||||||
];
|
|
||||||
|
|
||||||
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(customEmojis) as Set<string>).sort());
|
|
||||||
|
|
||||||
const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => {
|
|
||||||
if (node.current && !node.current.contains(e.target as Node)) {
|
|
||||||
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={clsx('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;
|
|
|
@ -1,73 +0,0 @@
|
||||||
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: MouseEvent | TouchEvent) => {
|
|
||||||
if (node.current && !node.current.contains(e.target as Node)) {
|
|
||||||
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;
|
|
|
@ -1,38 +0,0 @@
|
||||||
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;
|
|
|
@ -1,8 +1,7 @@
|
||||||
// @ts-ignore
|
import { List, Map } from 'immutable';
|
||||||
import { emojiIndex } from 'emoji-mart';
|
|
||||||
import pick from 'lodash/pick';
|
import pick from 'lodash/pick';
|
||||||
|
|
||||||
import { search } from '../emoji-mart-search-light';
|
import search, { addCustomToPool } from '../search';
|
||||||
|
|
||||||
const trimEmojis = (emoji: any) => pick(emoji, ['id', 'unified', 'native', 'custom']);
|
const trimEmojis = (emoji: any) => pick(emoji, ['id', 'unified', 'native', 'custom']);
|
||||||
|
|
||||||
|
@ -16,116 +15,83 @@ describe('emoji_index', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(search('pineapple').map(trimEmojis)).toEqual(expected);
|
expect(search('pineapple').map(trimEmojis)).toEqual(expected);
|
||||||
expect(emojiIndex.search('pineapple').map(trimEmojis)).toEqual(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('orders search results correctly', () => {
|
it('orders search results correctly', () => {
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
|
||||||
id: 'apple',
|
|
||||||
unified: '1f34e',
|
|
||||||
native: '🍎',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'pineapple',
|
id: 'pineapple',
|
||||||
unified: '1f34d',
|
unified: '1f34d',
|
||||||
native: '🍍',
|
native: '🍍',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'apple',
|
||||||
|
unified: '1f34e',
|
||||||
|
native: '🍎',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'green_apple',
|
id: 'green_apple',
|
||||||
unified: '1f34f',
|
unified: '1f34f',
|
||||||
native: '🍏',
|
native: '🍏',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'iphone',
|
|
||||||
unified: '1f4f1',
|
|
||||||
native: '📱',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
expect(search('apple').map(trimEmojis)).toEqual(expected);
|
expect(search('apple').map(trimEmojis)).toEqual(expected);
|
||||||
expect(emojiIndex.search('apple').map(trimEmojis)).toEqual(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can include/exclude categories', () => {
|
it('handles custom emojis', () => {
|
||||||
expect(search('flag', { include: ['people'] } as any)).toEqual([]);
|
|
||||||
expect(emojiIndex.search('flag', { include: ['people'] })).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('(different behavior from emoji-mart) do not erases custom emoji if not passed again', () => {
|
|
||||||
const custom = [
|
const custom = [
|
||||||
{
|
{
|
||||||
id: 'mastodon',
|
id: 'mastodon',
|
||||||
name: 'mastodon',
|
name: 'mastodon',
|
||||||
short_names: ['mastodon'],
|
|
||||||
text: '',
|
|
||||||
emoticons: [],
|
|
||||||
keywords: ['mastodon'],
|
keywords: ['mastodon'],
|
||||||
imageUrl: 'http://example.com',
|
skins: { src: 'http://example.com' },
|
||||||
custom: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
search('', { custom } as any);
|
|
||||||
emojiIndex.search('', { custom });
|
const custom_emojis = List([
|
||||||
|
Map({ static_url: 'http://example.com', shortcode: 'mastodon' }),
|
||||||
|
]);
|
||||||
|
|
||||||
const lightExpected = [
|
const lightExpected = [
|
||||||
{
|
{
|
||||||
id: 'mastodon',
|
id: 'mastodon',
|
||||||
custom: true,
|
custom: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(search('masto').map(trimEmojis)).toEqual(lightExpected);
|
|
||||||
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual([]);
|
addCustomToPool(custom);
|
||||||
|
expect(search('masto', {}, custom_emojis).map(trimEmojis)).toEqual(lightExpected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('(different behavior from emoji-mart) erases custom emoji if another is passed', () => {
|
it('updates custom emoji if another is passed', () => {
|
||||||
const custom = [
|
const custom = [
|
||||||
{
|
{
|
||||||
id: 'mastodon',
|
id: 'mastodon',
|
||||||
name: 'mastodon',
|
name: 'mastodon',
|
||||||
short_names: ['mastodon'],
|
|
||||||
text: '',
|
|
||||||
emoticons: [],
|
|
||||||
keywords: ['mastodon'],
|
keywords: ['mastodon'],
|
||||||
imageUrl: 'http://example.com',
|
skins: { src: 'http://example.com' },
|
||||||
custom: true,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
search('', { custom } as any);
|
|
||||||
emojiIndex.search('', { custom });
|
|
||||||
expect(search('masto', { custom: [] } as any).map(trimEmojis)).toEqual([]);
|
|
||||||
expect(emojiIndex.search('masto').map(trimEmojis)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles custom emoji', () => {
|
addCustomToPool(custom);
|
||||||
const custom = [
|
|
||||||
{
|
|
||||||
id: 'mastodon',
|
|
||||||
name: 'mastodon',
|
|
||||||
short_names: ['mastodon'],
|
|
||||||
text: '',
|
|
||||||
emoticons: [],
|
|
||||||
keywords: ['mastodon'],
|
|
||||||
imageUrl: 'http://example.com',
|
|
||||||
custom: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
search('', { custom } as any);
|
|
||||||
emojiIndex.search('', { custom });
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
id: 'mastodon',
|
|
||||||
custom: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
expect(search('masto', { custom } as any).map(trimEmojis)).toEqual(expected);
|
|
||||||
expect(emojiIndex.search('masto', { custom }).map(trimEmojis)).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should filter only emojis we care about, exclude pineapple', () => {
|
const custom2 = [
|
||||||
const emojisToShowFilter = (emoji: any) => emoji.unified !== '1F34D';
|
{
|
||||||
expect(search('apple', { emojisToShowFilter } as any).map((obj: any) => obj.id))
|
id: 'pleroma',
|
||||||
.not.toContain('pineapple');
|
name: 'pleroma',
|
||||||
expect(emojiIndex.search('apple', { emojisToShowFilter }).map((obj: any) => obj.id))
|
keywords: ['pleroma'],
|
||||||
.not.toContain('pineapple');
|
skins: { src: 'http://example.com' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
addCustomToPool(custom2);
|
||||||
|
|
||||||
|
const custom_emojis = List([
|
||||||
|
Map({ static_url: 'http://example.com', shortcode: 'pleroma' }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const expected: any = [];
|
||||||
|
expect(search('masto', {}, custom_emojis).map(trimEmojis)).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji whose unified name is irregular', () => {
|
it('does an emoji whose unified name is irregular', () => {
|
||||||
|
@ -147,7 +113,6 @@ describe('emoji_index', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(search('polo').map(trimEmojis)).toEqual(expected);
|
expect(search('polo').map(trimEmojis)).toEqual(expected);
|
||||||
expect(emojiIndex.search('polo').map(trimEmojis)).toEqual(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can search for thinking_face', () => {
|
it('can search for thinking_face', () => {
|
||||||
|
@ -159,7 +124,6 @@ describe('emoji_index', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
expect(search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
||||||
expect(emojiIndex.search('thinking_fac').map(trimEmojis)).toEqual(expected);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can search for woman-facepalming', () => {
|
it('can search for woman-facepalming', () => {
|
||||||
|
@ -171,6 +135,5 @@ describe('emoji_index', () => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
|
expect(search('woman-facep').map(trimEmojis)).toEqual(expected);
|
||||||
expect(emojiIndex.search('woman-facep').map(trimEmojis)).toEqual(expected);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,259 @@
|
||||||
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
import React, { useEffect, useState, useLayoutEffect } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import { useEmoji } from 'soapbox/actions/emojis';
|
||||||
|
import { changeSetting } from 'soapbox/actions/settings';
|
||||||
|
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
||||||
|
import { RootState } from 'soapbox/store';
|
||||||
|
|
||||||
|
import { buildCustomEmojis } from '../../emoji';
|
||||||
|
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
|
||||||
|
|
||||||
|
import type { State as PopperState } from '@popperjs/core';
|
||||||
|
import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji';
|
||||||
|
|
||||||
|
let EmojiPicker: any; // load asynchronously
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
emoji_pick: { id: 'emoji_button.pick', defaultMessage: 'Pick an emoji…' },
|
||||||
|
emoji_oh_no: { id: 'emoji_button.oh_no', defaultMessage: 'Oh no!' },
|
||||||
|
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
||||||
|
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emoji\'s found.' },
|
||||||
|
emoji_add_custom: { id: 'emoji_button.add_custom', defaultMessage: 'Add custom emoji' },
|
||||||
|
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_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' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface IEmojiPickerDropdown {
|
||||||
|
onPickEmoji?: (emoji: Emoji) => void
|
||||||
|
condensed?: boolean
|
||||||
|
withCustom?: boolean
|
||||||
|
visible: boolean
|
||||||
|
setVisible: (value: boolean) => void
|
||||||
|
update: (() => Promise<Partial<PopperState>>) | null
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getFrequentlyUsedEmojis = createSelector([
|
||||||
|
(state: RootState) => state.settings.get('frequentlyUsedEmojis', ImmutableMap()),
|
||||||
|
], (emojiCounters: ImmutableMap<string, number>) => {
|
||||||
|
let emojis = emojiCounters
|
||||||
|
.keySeq()
|
||||||
|
.sort((a, b) => 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,
|
||||||
|
], 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;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({
|
||||||
|
onPickEmoji, visible, setVisible, update, withCustom = true,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const settings = useSettings();
|
||||||
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
const userTheme = settings.get('themeMode');
|
||||||
|
const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto';
|
||||||
|
|
||||||
|
const customEmojis = useAppSelector((state) => getCustomEmojis(state));
|
||||||
|
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handlePick = (emoji: any) => {
|
||||||
|
setVisible(false);
|
||||||
|
|
||||||
|
let pickedEmoji: Emoji;
|
||||||
|
|
||||||
|
if (emoji.native) {
|
||||||
|
pickedEmoji = {
|
||||||
|
id: emoji.id,
|
||||||
|
colons: emoji.shortcodes,
|
||||||
|
custom: false,
|
||||||
|
native: emoji.native,
|
||||||
|
unified: emoji.unified,
|
||||||
|
} as NativeEmoji;
|
||||||
|
} else {
|
||||||
|
pickedEmoji = {
|
||||||
|
id: emoji.id,
|
||||||
|
colons: emoji.shortcodes,
|
||||||
|
custom: true,
|
||||||
|
imageUrl: emoji.src,
|
||||||
|
} as CustomEmoji;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(useEmoji(pickedEmoji)); // eslint-disable-line react-hooks/rules-of-hooks
|
||||||
|
|
||||||
|
if (onPickEmoji) {
|
||||||
|
onPickEmoji(pickedEmoji);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkinTone = (skinTone: string) => {
|
||||||
|
dispatch(changeSetting(['skinTone'], skinTone));
|
||||||
|
};
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// fix scrolling focus issue
|
||||||
|
if (visible) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EmojiPicker) {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
EmojiPickerAsync().then(EmojiMart => {
|
||||||
|
EmojiPicker = EmojiMart.Picker;
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}).catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
visible ? (
|
||||||
|
<RenderAfter update={update}>
|
||||||
|
{!loading && (
|
||||||
|
<EmojiPicker
|
||||||
|
custom={withCustom ? [{ emojis: buildCustomEmojis(customEmojis) }] : undefined}
|
||||||
|
title={title}
|
||||||
|
onEmojiSelect={handlePick}
|
||||||
|
recent={frequentlyUsedEmojis}
|
||||||
|
perLine={8}
|
||||||
|
skin={handleSkinTone}
|
||||||
|
emojiSize={22}
|
||||||
|
emojiButtonSize={34}
|
||||||
|
set='twitter'
|
||||||
|
theme={theme}
|
||||||
|
i18n={getI18n()}
|
||||||
|
skinTonePosition='search'
|
||||||
|
previewPosition='none'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RenderAfter>
|
||||||
|
) : null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmojiPickerDropdown;
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { Picker as EmojiPicker } from 'emoji-mart';
|
||||||
|
import React, { useRef, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { joinPublicPath } from 'soapbox/utils/static';
|
||||||
|
|
||||||
|
import data from '../data';
|
||||||
|
|
||||||
|
const getSpritesheetURL = (set: string) => {
|
||||||
|
return require('emoji-datasource/img/twitter/sheets/32.png');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getImageURL = (set: string, name: string) => {
|
||||||
|
return joinPublicPath(`/packs/emoji/${name}.svg`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Picker = (props: any) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const input = { ...props, data, ref, getImageURL, getSpritesheetURL };
|
||||||
|
|
||||||
|
new EmojiPicker(input);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <div ref={ref} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Picker,
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
import React, { KeyboardEvent, useEffect, useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { usePopper } from 'react-popper';
|
||||||
|
|
||||||
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
|
import { isMobile } from 'soapbox/is-mobile';
|
||||||
|
|
||||||
|
import EmojiPickerDropdown, { IEmojiPickerDropdown } from '../components/emoji-picker-dropdown';
|
||||||
|
|
||||||
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
|
export const messages = defineMessages({
|
||||||
|
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const EmojiPickerDropdownContainer = (
|
||||||
|
props: Pick<IEmojiPickerDropdown, 'onPickEmoji' | 'condensed' | 'withCustom'>,
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
|
||||||
|
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
const [popperReference, setPopperReference] = useState<HTMLButtonElement | null>(null);
|
||||||
|
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
const placement = props.condensed ? 'bottom-start' : 'top-start';
|
||||||
|
const { styles, attributes, update } = usePopper(popperReference, popperElement, {
|
||||||
|
placement: isMobile(window.innerWidth) ? 'auto' : placement,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDocClick = (e: any) => {
|
||||||
|
if (!containerElement?.contains(e.target) && !popperElement?.contains(e.target)) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggle = (e: MouseEvent | KeyboardEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setVisible(!visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: move to class
|
||||||
|
const style: React.CSSProperties = !isMobile(window.innerWidth) ? styles.popper : {
|
||||||
|
...styles.popper, width: '100%',
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', handleDocClick, false);
|
||||||
|
document.addEventListener('touchend', handleDocClick, listenerOptions);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleDocClick, false);
|
||||||
|
// @ts-ignore
|
||||||
|
document.removeEventListener('touchend', handleDocClick, listenerOptions);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='relative' ref={setContainerElement}>
|
||||||
|
<IconButton
|
||||||
|
className={clsx({
|
||||||
|
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||||
|
})}
|
||||||
|
ref={setPopperReference}
|
||||||
|
src={require('@tabler/icons/mood-happy.svg')}
|
||||||
|
title={title}
|
||||||
|
aria-label={title}
|
||||||
|
aria-expanded={visible}
|
||||||
|
role='button'
|
||||||
|
onClick={handleToggle as any}
|
||||||
|
onKeyDown={handleToggle as React.KeyboardEventHandler<HTMLButtonElement>}
|
||||||
|
tabIndex={0}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{createPortal(
|
||||||
|
<div
|
||||||
|
className='z-[101]'
|
||||||
|
ref={setPopperElement}
|
||||||
|
style={style}
|
||||||
|
{...attributes.popper}
|
||||||
|
>
|
||||||
|
<EmojiPickerDropdown visible={visible} setVisible={setVisible} update={update} {...props} />
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default EmojiPickerDropdownContainer;
|
|
@ -0,0 +1,52 @@
|
||||||
|
import data from '@emoji-mart/data/sets/14/twitter.json';
|
||||||
|
|
||||||
|
export interface NativeEmoji {
|
||||||
|
unified: string
|
||||||
|
native: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomEmoji {
|
||||||
|
src: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Emoji<T> {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
keywords: string[]
|
||||||
|
skins: T[]
|
||||||
|
version?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiCategory {
|
||||||
|
id: string
|
||||||
|
emojis: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiMap {
|
||||||
|
[s: string]: Emoji<NativeEmoji>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiAlias {
|
||||||
|
[s: string]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiSheet {
|
||||||
|
cols: number
|
||||||
|
rows: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmojiData {
|
||||||
|
categories: EmojiCategory[]
|
||||||
|
emojis: EmojiMap
|
||||||
|
aliases: EmojiAlias
|
||||||
|
sheet: EmojiSheet
|
||||||
|
}
|
||||||
|
|
||||||
|
const emojiData = data as EmojiData;
|
||||||
|
const { categories, emojis, aliases, sheet } = emojiData;
|
||||||
|
|
||||||
|
export { categories, emojis, aliases, sheet };
|
||||||
|
|
||||||
|
export default emojiData;
|
|
@ -1,124 +0,0 @@
|
||||||
// @preval
|
|
||||||
// http://www.unicode.org/Public/emoji/5.0/emoji-test.txt
|
|
||||||
// This file contains the compressed version of the emoji data from
|
|
||||||
// both emoji-map.json and from emoji-mart's emojiIndex and data objects.
|
|
||||||
// It's designed to be emitted in an array format to take up less space
|
|
||||||
// over the wire.
|
|
||||||
|
|
||||||
const { emojiIndex } = require('emoji-mart');
|
|
||||||
let data = require('emoji-mart/data/all.json');
|
|
||||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
|
||||||
|
|
||||||
const emojiMap = require('./emoji-map.json');
|
|
||||||
const { unicodeToFilename } = require('./unicode-to-filename');
|
|
||||||
const { unicodeToUnifiedName } = require('./unicode-to-unified-name');
|
|
||||||
|
|
||||||
if (data.compressed) {
|
|
||||||
data = emojiMartUncompress(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojiMartData = data;
|
|
||||||
|
|
||||||
const excluded = ['®', '©', '™'];
|
|
||||||
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
|
|
||||||
const shortcodeMap = {};
|
|
||||||
|
|
||||||
const shortCodesToEmojiData = {};
|
|
||||||
const emojisWithoutShortCodes = [];
|
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
|
||||||
let emoji = emojiIndex.emojis[key];
|
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
|
||||||
if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
shortcodeMap[emoji.native] = emoji.id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const stripModifiers = unicode => {
|
|
||||||
skinTones.forEach(tone => {
|
|
||||||
unicode = unicode.replace(tone, '');
|
|
||||||
});
|
|
||||||
|
|
||||||
return unicode;
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.keys(emojiMap).forEach(key => {
|
|
||||||
if (excluded.includes(key)) {
|
|
||||||
delete emojiMap[key];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedKey = stripModifiers(key);
|
|
||||||
let shortcode = shortcodeMap[normalizedKey];
|
|
||||||
|
|
||||||
if (!shortcode) {
|
|
||||||
shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const filename = emojiMap[key];
|
|
||||||
|
|
||||||
const filenameData = [key];
|
|
||||||
|
|
||||||
if (unicodeToFilename(key) !== filename) {
|
|
||||||
// filename can't be derived using unicodeToFilename
|
|
||||||
filenameData.push(filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof shortcode === 'undefined') {
|
|
||||||
emojisWithoutShortCodes.push(filenameData);
|
|
||||||
} else {
|
|
||||||
if (!Array.isArray(shortCodesToEmojiData[shortcode])) {
|
|
||||||
shortCodesToEmojiData[shortcode] = [[]];
|
|
||||||
}
|
|
||||||
|
|
||||||
shortCodesToEmojiData[shortcode][0].push(filenameData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(emojiIndex.emojis).forEach(key => {
|
|
||||||
let emoji = emojiIndex.emojis[key];
|
|
||||||
|
|
||||||
// Emojis with skin tone modifiers are stored like this
|
|
||||||
if (Object.prototype.hasOwnProperty.call(emoji, '1')) {
|
|
||||||
emoji = emoji['1'];
|
|
||||||
}
|
|
||||||
|
|
||||||
const { native } = emoji;
|
|
||||||
const { short_names, search, unified } = emojiMartData.emojis[key];
|
|
||||||
|
|
||||||
if (short_names[0] !== key) {
|
|
||||||
throw new Error('The compresser expects the first short_code to be the ' +
|
|
||||||
'key. It may need to be rewritten if the emoji change such that this ' +
|
|
||||||
'is no longer the case.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchData = [
|
|
||||||
native,
|
|
||||||
short_names.slice(1), // first short name can be inferred from the key
|
|
||||||
search,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (unicodeToUnifiedName(native) !== unified) {
|
|
||||||
// unified name can't be derived from unicodeToUnifiedName
|
|
||||||
searchData.push(unified);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(shortCodesToEmojiData[key])) {
|
|
||||||
shortCodesToEmojiData[key] = [[]];
|
|
||||||
}
|
|
||||||
|
|
||||||
shortCodesToEmojiData[key].push(searchData);
|
|
||||||
});
|
|
||||||
|
|
||||||
// JSON.parse/stringify is to emulate what @preval is doing and avoid any
|
|
||||||
// inconsistent behavior in dev mode
|
|
||||||
module.exports = JSON.parse(JSON.stringify([
|
|
||||||
shortCodesToEmojiData,
|
|
||||||
emojiMartData.skins,
|
|
||||||
emojiMartData.categories,
|
|
||||||
emojiMartData.aliases,
|
|
||||||
emojisWithoutShortCodes,
|
|
||||||
]));
|
|
File diff suppressed because one or more lines are too long
|
@ -1,44 +0,0 @@
|
||||||
// The output of this module is designed to mimic emoji-mart's
|
|
||||||
// "data" object, such that we can use it for a light version of emoji-mart's
|
|
||||||
// emojiIndex.search functionality.
|
|
||||||
import emojiCompressed from './emoji-compressed';
|
|
||||||
import { unicodeToUnifiedName } from './unicode-to-unified-name';
|
|
||||||
|
|
||||||
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
|
|
||||||
|
|
||||||
const emojis: Record<string, any> = {};
|
|
||||||
|
|
||||||
// decompress
|
|
||||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
|
||||||
const [
|
|
||||||
_filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
searchData,
|
|
||||||
] = shortCodesToEmojiData[shortCode];
|
|
||||||
const [
|
|
||||||
native,
|
|
||||||
short_names,
|
|
||||||
search,
|
|
||||||
unified,
|
|
||||||
] = searchData;
|
|
||||||
|
|
||||||
emojis[shortCode] = {
|
|
||||||
native,
|
|
||||||
search,
|
|
||||||
short_names: [shortCode].concat(short_names),
|
|
||||||
unified: unified || unicodeToUnifiedName(native),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export {
|
|
||||||
emojis,
|
|
||||||
skins,
|
|
||||||
categories,
|
|
||||||
short_names,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
emojis,
|
|
||||||
skins,
|
|
||||||
categories,
|
|
||||||
short_names,
|
|
||||||
};
|
|
|
@ -1,183 +0,0 @@
|
||||||
// This code is largely borrowed from:
|
|
||||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
|
|
||||||
|
|
||||||
import data from './emoji-mart-data-light';
|
|
||||||
import { getData, getSanitizedData, uniq, intersect } from './emoji-utils';
|
|
||||||
|
|
||||||
const originalPool = {};
|
|
||||||
let index = {};
|
|
||||||
const emojisList = {};
|
|
||||||
const emoticonsList = {};
|
|
||||||
let customEmojisList = [];
|
|
||||||
|
|
||||||
for (const emoji in data.emojis) {
|
|
||||||
const emojiData = data.emojis[emoji];
|
|
||||||
const { short_names, emoticons } = emojiData;
|
|
||||||
const id = short_names[0];
|
|
||||||
|
|
||||||
if (emoticons) {
|
|
||||||
emoticons.forEach(emoticon => {
|
|
||||||
if (emoticonsList[emoticon]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
emoticonsList[emoticon] = id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emojisList[id] = getSanitizedData(id);
|
|
||||||
originalPool[id] = emojiData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearCustomEmojis(pool) {
|
|
||||||
customEmojisList.forEach((emoji) => {
|
|
||||||
const emojiId = emoji.id || emoji.short_names[0];
|
|
||||||
|
|
||||||
delete pool[emojiId];
|
|
||||||
delete emojisList[emojiId];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addCustomToPool(custom, pool = originalPool) {
|
|
||||||
if (customEmojisList.length) clearCustomEmojis(pool);
|
|
||||||
|
|
||||||
custom.forEach((emoji) => {
|
|
||||||
const emojiId = emoji.id || emoji.short_names[0];
|
|
||||||
|
|
||||||
if (emojiId && !pool[emojiId]) {
|
|
||||||
pool[emojiId] = getData(emoji);
|
|
||||||
emojisList[emojiId] = getSanitizedData(emoji);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
customEmojisList = custom;
|
|
||||||
index = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function search(value, { emojisToShowFilter, maxResults, include, exclude, custom } = {}) {
|
|
||||||
if (custom !== undefined) {
|
|
||||||
if (customEmojisList !== custom)
|
|
||||||
addCustomToPool(custom, originalPool);
|
|
||||||
} else {
|
|
||||||
custom = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
maxResults = maxResults || 75;
|
|
||||||
include = include || [];
|
|
||||||
exclude = exclude || [];
|
|
||||||
|
|
||||||
let results = null,
|
|
||||||
pool = originalPool;
|
|
||||||
|
|
||||||
if (value.length) {
|
|
||||||
if (value === '-' || value === '-1') {
|
|
||||||
return [emojisList['-1']];
|
|
||||||
}
|
|
||||||
|
|
||||||
let values = value.toLowerCase().split(/[\s|,|\-|_]+/),
|
|
||||||
allResults = [];
|
|
||||||
|
|
||||||
if (values.length > 2) {
|
|
||||||
values = [values[0], values[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (include.length || exclude.length) {
|
|
||||||
pool = {};
|
|
||||||
|
|
||||||
data.categories.forEach(category => {
|
|
||||||
const isIncluded = include && include.length ? include.includes(category.name.toLowerCase()) : true;
|
|
||||||
const isExcluded = exclude && exclude.length ? exclude.includes(category.name.toLowerCase()) : false;
|
|
||||||
if (!isIncluded || isExcluded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (custom.length) {
|
|
||||||
const customIsIncluded = include && include.length ? include.includes('custom') : true;
|
|
||||||
const customIsExcluded = exclude && exclude.length ? exclude.includes('custom') : false;
|
|
||||||
if (customIsIncluded && !customIsExcluded) {
|
|
||||||
addCustomToPool(custom, pool);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchValue = (value) => {
|
|
||||||
let aPool = pool,
|
|
||||||
aIndex = index,
|
|
||||||
length = 0;
|
|
||||||
|
|
||||||
for (let charIndex = 0; charIndex < value.length; charIndex++) {
|
|
||||||
const char = value[charIndex];
|
|
||||||
length++;
|
|
||||||
|
|
||||||
aIndex[char] = aIndex[char] || {};
|
|
||||||
aIndex = aIndex[char];
|
|
||||||
|
|
||||||
if (!aIndex.results) {
|
|
||||||
const scores = {};
|
|
||||||
|
|
||||||
aIndex.results = [];
|
|
||||||
aIndex.pool = {};
|
|
||||||
|
|
||||||
for (const id in aPool) {
|
|
||||||
const emoji = aPool[id],
|
|
||||||
{ search } = emoji,
|
|
||||||
sub = value.substr(0, length),
|
|
||||||
subIndex = search.indexOf(sub);
|
|
||||||
|
|
||||||
if (subIndex !== -1) {
|
|
||||||
let score = subIndex + 1;
|
|
||||||
if (sub === id) score = 0;
|
|
||||||
|
|
||||||
aIndex.results.push(emojisList[id]);
|
|
||||||
aIndex.pool[id] = emoji;
|
|
||||||
|
|
||||||
scores[id] = score;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aIndex.results.sort((a, b) => {
|
|
||||||
const aScore = scores[a.id],
|
|
||||||
bScore = scores[b.id];
|
|
||||||
|
|
||||||
return aScore - bScore;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
aPool = aIndex.pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
return aIndex.results;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (values.length > 1) {
|
|
||||||
results = searchValue(value);
|
|
||||||
} else {
|
|
||||||
results = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
allResults = values.map(searchValue).filter(a => a);
|
|
||||||
|
|
||||||
if (allResults.length > 1) {
|
|
||||||
allResults = intersect.apply(null, allResults);
|
|
||||||
} else if (allResults.length) {
|
|
||||||
allResults = allResults[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
results = uniq(results.concat(allResults));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results) {
|
|
||||||
if (emojisToShowFilter) {
|
|
||||||
results = results.filter((result) => emojisToShowFilter(data.emojis[result.id]));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results && results.length > maxResults) {
|
|
||||||
results = results.slice(0, maxResults);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// @ts-ignore no types
|
|
||||||
import Emoji from 'emoji-mart/dist-es/components/emoji/emoji';
|
|
||||||
// @ts-ignore no types
|
|
||||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
|
||||||
|
|
||||||
export {
|
|
||||||
Picker,
|
|
||||||
Emoji,
|
|
||||||
};
|
|
|
@ -1,32 +0,0 @@
|
||||||
// A mapping of unicode strings to an object containing the filename
|
|
||||||
// (i.e. the svg filename) and a shortCode intended to be shown
|
|
||||||
// as a "title" attribute in an HTML element (aka tooltip).
|
|
||||||
|
|
||||||
const [
|
|
||||||
shortCodesToEmojiData,
|
|
||||||
skins, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
categories, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
short_names, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
||||||
emojisWithoutShortCodes,
|
|
||||||
] = require('./emoji-compressed');
|
|
||||||
const { unicodeToFilename } = require('./unicode-to-filename');
|
|
||||||
|
|
||||||
// decompress
|
|
||||||
const unicodeMapping = {};
|
|
||||||
|
|
||||||
function processEmojiMapData(emojiMapData, shortCode) {
|
|
||||||
const [ native, filename ] = emojiMapData;
|
|
||||||
|
|
||||||
unicodeMapping[native] = {
|
|
||||||
shortCode,
|
|
||||||
filename: filename || unicodeToFilename(native),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
|
|
||||||
const [ filenameData ] = shortCodesToEmojiData[shortCode];
|
|
||||||
filenameData.forEach(emojiMapData => processEmojiMapData(emojiMapData, shortCode));
|
|
||||||
});
|
|
||||||
emojisWithoutShortCodes.forEach(emojiMapData => processEmojiMapData(emojiMapData));
|
|
||||||
|
|
||||||
module.exports = unicodeMapping;
|
|
|
@ -1,253 +0,0 @@
|
||||||
// This code is largely borrowed from:
|
|
||||||
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/index.js
|
|
||||||
|
|
||||||
import data from './emoji-mart-data-light';
|
|
||||||
|
|
||||||
const buildSearch = (data) => {
|
|
||||||
const search = [];
|
|
||||||
|
|
||||||
const addToSearch = (strings, split) => {
|
|
||||||
if (!strings) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
(Array.isArray(strings) ? strings : [strings]).forEach((string) => {
|
|
||||||
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
|
||||||
s = s.toLowerCase();
|
|
||||||
|
|
||||||
if (!search.includes(s)) {
|
|
||||||
search.push(s);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
addToSearch(data.short_names, true);
|
|
||||||
addToSearch(data.name, true);
|
|
||||||
addToSearch(data.keywords, false);
|
|
||||||
addToSearch(data.emoticons, false);
|
|
||||||
|
|
||||||
return search.join(',');
|
|
||||||
};
|
|
||||||
|
|
||||||
const _String = String;
|
|
||||||
|
|
||||||
const stringFromCodePoint = _String.fromCodePoint || function() {
|
|
||||||
const MAX_SIZE = 0x4000;
|
|
||||||
const codeUnits = [];
|
|
||||||
let highSurrogate;
|
|
||||||
let lowSurrogate;
|
|
||||||
let index = -1;
|
|
||||||
const length = arguments.length;
|
|
||||||
if (!length) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
let result = '';
|
|
||||||
while (++index < length) {
|
|
||||||
let codePoint = Number(arguments[index]);
|
|
||||||
if (
|
|
||||||
!isFinite(codePoint) || // `NaN`, `+Infinity`, or `-Infinity`
|
|
||||||
codePoint < 0 || // not a valid Unicode code point
|
|
||||||
codePoint > 0x10FFFF || // not a valid Unicode code point
|
|
||||||
Math.floor(codePoint) !== codePoint // not an integer
|
|
||||||
) {
|
|
||||||
throw RangeError('Invalid code point: ' + codePoint);
|
|
||||||
}
|
|
||||||
if (codePoint <= 0xFFFF) { // BMP code point
|
|
||||||
codeUnits.push(codePoint);
|
|
||||||
} else { // Astral code point; split in surrogate halves
|
|
||||||
// http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae
|
|
||||||
codePoint -= 0x10000;
|
|
||||||
highSurrogate = (codePoint >> 10) + 0xD800;
|
|
||||||
lowSurrogate = (codePoint % 0x400) + 0xDC00;
|
|
||||||
codeUnits.push(highSurrogate, lowSurrogate);
|
|
||||||
}
|
|
||||||
if (index + 1 === length || codeUnits.length > MAX_SIZE) {
|
|
||||||
result += String.fromCharCode.apply(null, codeUnits);
|
|
||||||
codeUnits.length = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _JSON = JSON;
|
|
||||||
|
|
||||||
const COLONS_REGEX = /^(?:\:([^\:]+)\:)(?:\:skin-tone-(\d)\:)?$/;
|
|
||||||
const SKINS = [
|
|
||||||
'1F3FA', '1F3FB', '1F3FC',
|
|
||||||
'1F3FD', '1F3FE', '1F3FF',
|
|
||||||
];
|
|
||||||
|
|
||||||
function unifiedToNative(unified) {
|
|
||||||
const unicodes = unified.split('-'),
|
|
||||||
codePoints = unicodes.map((u) => `0x${u}`);
|
|
||||||
|
|
||||||
return stringFromCodePoint.apply(null, codePoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitize(emoji) {
|
|
||||||
const { name, short_names, skin_tone, skin_variations, emoticons, unified, custom, imageUrl } = emoji;
|
|
||||||
const id = emoji.id || short_names[0];
|
|
||||||
const colons = `:${id}:`;
|
|
||||||
|
|
||||||
if (custom) {
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
colons,
|
|
||||||
emoticons,
|
|
||||||
custom,
|
|
||||||
imageUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
colons: skin_tone ? `${colons}:skin-tone-${skin_tone}:` : colons,
|
|
||||||
emoticons,
|
|
||||||
unified: unified.toLowerCase(),
|
|
||||||
skin: skin_tone || (skin_variations ? 1 : null),
|
|
||||||
native: unifiedToNative(unified),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSanitizedData() {
|
|
||||||
return sanitize(getData(...arguments));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getData(emoji, skin, set) {
|
|
||||||
let emojiData = {};
|
|
||||||
|
|
||||||
if (typeof emoji === 'string') {
|
|
||||||
const matches = emoji.match(COLONS_REGEX);
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
emoji = matches[1];
|
|
||||||
|
|
||||||
if (matches[2]) {
|
|
||||||
skin = parseInt(matches[2]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data.short_names, emoji)) {
|
|
||||||
emoji = data.short_names[emoji];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data.emojis, emoji)) {
|
|
||||||
emojiData = data.emojis[emoji];
|
|
||||||
}
|
|
||||||
} else if (emoji.id) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data.short_names, emoji.id)) {
|
|
||||||
emoji.id = data.short_names[emoji.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(data.emojis, emoji.id)) {
|
|
||||||
emojiData = data.emojis[emoji.id];
|
|
||||||
skin = skin || emoji.skin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Object.keys(emojiData).length) {
|
|
||||||
emojiData = emoji;
|
|
||||||
emojiData.custom = true;
|
|
||||||
|
|
||||||
if (!emojiData.search) {
|
|
||||||
emojiData.search = buildSearch(emoji);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emojiData.emoticons = emojiData.emoticons || [];
|
|
||||||
emojiData.variations = emojiData.variations || [];
|
|
||||||
|
|
||||||
if (emojiData.skin_variations && skin > 1 && set) {
|
|
||||||
emojiData = JSON.parse(_JSON.stringify(emojiData));
|
|
||||||
|
|
||||||
const skinKey = SKINS[skin - 1],
|
|
||||||
variationData = emojiData.skin_variations[skinKey];
|
|
||||||
|
|
||||||
if (!variationData.variations && emojiData.variations) {
|
|
||||||
delete emojiData.variations;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (variationData[`has_img_${set}`]) {
|
|
||||||
emojiData.skin_tone = skin;
|
|
||||||
|
|
||||||
for (const k in variationData) {
|
|
||||||
const v = variationData[k];
|
|
||||||
emojiData[k] = v;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (emojiData.variations && emojiData.variations.length) {
|
|
||||||
emojiData = JSON.parse(_JSON.stringify(emojiData));
|
|
||||||
emojiData.unified = emojiData.variations.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
return emojiData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function uniq(arr) {
|
|
||||||
return arr.reduce((acc, item) => {
|
|
||||||
if (!acc.includes(item)) {
|
|
||||||
acc.push(item);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function intersect(a, b) {
|
|
||||||
const uniqA = uniq(a);
|
|
||||||
const uniqB = uniq(b);
|
|
||||||
|
|
||||||
return uniqA.filter(item => uniqB.includes(item));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deepMerge(a, b) {
|
|
||||||
const o = {};
|
|
||||||
|
|
||||||
for (const key in a) {
|
|
||||||
const originalValue = a[key];
|
|
||||||
let value = originalValue;
|
|
||||||
|
|
||||||
if (Object.prototype.hasOwnProperty.call(b, key)) {
|
|
||||||
value = b[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'object') {
|
|
||||||
value = deepMerge(originalValue, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
o[key] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/sonicdoe/measure-scrollbar
|
|
||||||
function measureScrollbar() {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
|
|
||||||
div.style.width = '100px';
|
|
||||||
div.style.height = '100px';
|
|
||||||
div.style.overflow = 'scroll';
|
|
||||||
div.style.position = 'absolute';
|
|
||||||
div.style.top = '-9999px';
|
|
||||||
|
|
||||||
document.body.appendChild(div);
|
|
||||||
const scrollbarWidth = div.offsetWidth - div.clientWidth;
|
|
||||||
document.body.removeChild(div);
|
|
||||||
|
|
||||||
return scrollbarWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getData,
|
|
||||||
getSanitizedData,
|
|
||||||
uniq,
|
|
||||||
intersect,
|
|
||||||
deepMerge,
|
|
||||||
unifiedToNative,
|
|
||||||
measureScrollbar,
|
|
||||||
};
|
|
|
@ -1,132 +0,0 @@
|
||||||
import Trie from 'substring-trie';
|
|
||||||
|
|
||||||
import { joinPublicPath } from 'soapbox/utils/static';
|
|
||||||
|
|
||||||
import unicodeMapping from './emoji-unicode-mapping-light';
|
|
||||||
|
|
||||||
const trie = new Trie(Object.keys(unicodeMapping));
|
|
||||||
|
|
||||||
const emojifyTextNode = (node, customEmojis, autoPlayGif = false) => {
|
|
||||||
let str = node.textContent;
|
|
||||||
|
|
||||||
const fragment = new DocumentFragment();
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
let match, i = 0;
|
|
||||||
|
|
||||||
if (customEmojis === null) {
|
|
||||||
while (i < str.length && !(match = trie.search(str.slice(i)))) {
|
|
||||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
while (i < str.length && str[i] !== ':' && !(match = trie.search(str.slice(i)))) {
|
|
||||||
i += str.codePointAt(i) < 65536 ? 1 : 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rend, replacement = null;
|
|
||||||
if (i === str.length) {
|
|
||||||
break;
|
|
||||||
} else if (str[i] === ':') {
|
|
||||||
// eslint-disable-next-line no-loop-func
|
|
||||||
if (!(() => {
|
|
||||||
rend = str.indexOf(':', i + 1) + 1;
|
|
||||||
if (!rend) return false; // no pair of ':'
|
|
||||||
const shortname = str.slice(i, rend);
|
|
||||||
// now got a replacee as ':shortname:'
|
|
||||||
// if you want additional emoji handler, add statements below which set replacement and return true.
|
|
||||||
if (shortname in customEmojis) {
|
|
||||||
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
|
|
||||||
replacement = document.createElement('img');
|
|
||||||
replacement.setAttribute('draggable', false);
|
|
||||||
replacement.setAttribute('class', 'emojione custom-emoji');
|
|
||||||
replacement.setAttribute('alt', shortname);
|
|
||||||
replacement.setAttribute('title', shortname);
|
|
||||||
replacement.setAttribute('src', filename);
|
|
||||||
replacement.setAttribute('data-original', customEmojis[shortname].url);
|
|
||||||
replacement.setAttribute('data-static', customEmojis[shortname].static_url);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
})()) rend = ++i;
|
|
||||||
} else { // matched to unicode emoji
|
|
||||||
const { filename, shortCode } = unicodeMapping[match];
|
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
|
||||||
replacement = document.createElement('img');
|
|
||||||
replacement.setAttribute('draggable', false);
|
|
||||||
replacement.setAttribute('class', 'emojione');
|
|
||||||
replacement.setAttribute('alt', match);
|
|
||||||
replacement.setAttribute('title', title);
|
|
||||||
replacement.setAttribute('src', joinPublicPath(`packs/emoji/${filename}.svg`));
|
|
||||||
rend = i + match.length;
|
|
||||||
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
|
|
||||||
if (str.codePointAt(rend) === 65038) {
|
|
||||||
rend += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment.append(document.createTextNode(str.slice(0, i)));
|
|
||||||
if (replacement) {
|
|
||||||
fragment.append(replacement);
|
|
||||||
}
|
|
||||||
node.textContent = str.slice(0, i);
|
|
||||||
str = str.slice(rend);
|
|
||||||
}
|
|
||||||
|
|
||||||
fragment.append(document.createTextNode(str));
|
|
||||||
node.parentElement.replaceChild(fragment, node);
|
|
||||||
};
|
|
||||||
|
|
||||||
const emojifyNode = (node, customEmojis, autoPlayGif = false) => {
|
|
||||||
for (const child of node.childNodes) {
|
|
||||||
switch (child.nodeType) {
|
|
||||||
case Node.TEXT_NODE:
|
|
||||||
emojifyTextNode(child, customEmojis, autoPlayGif);
|
|
||||||
break;
|
|
||||||
case Node.ELEMENT_NODE:
|
|
||||||
if (!child.classList.contains('invisible'))
|
|
||||||
emojifyNode(child, customEmojis, autoPlayGif);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const emojify = (str, customEmojis = {}, autoPlayGif = false) => {
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.innerHTML = str;
|
|
||||||
|
|
||||||
if (!Object.keys(customEmojis).length)
|
|
||||||
customEmojis = null;
|
|
||||||
|
|
||||||
emojifyNode(wrapper, customEmojis, autoPlayGif);
|
|
||||||
|
|
||||||
return wrapper.innerHTML;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default emojify;
|
|
||||||
|
|
||||||
export const buildCustomEmojis = (customEmojis, autoPlayGif = false) => {
|
|
||||||
const emojis = [];
|
|
||||||
|
|
||||||
customEmojis.forEach(emoji => {
|
|
||||||
const shortcode = emoji.get('shortcode');
|
|
||||||
const url = autoPlayGif ? emoji.get('url') : emoji.get('static_url');
|
|
||||||
const name = shortcode.replace(':', '');
|
|
||||||
|
|
||||||
emojis.push({
|
|
||||||
id: name,
|
|
||||||
name,
|
|
||||||
short_names: [name],
|
|
||||||
text: '',
|
|
||||||
emoticons: [],
|
|
||||||
keywords: [name],
|
|
||||||
imageUrl: url,
|
|
||||||
custom: true,
|
|
||||||
customCategory: emoji.get('category'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return emojis;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set(['custom']));
|
|
|
@ -0,0 +1,228 @@
|
||||||
|
import split from 'graphemesplit';
|
||||||
|
|
||||||
|
import unicodeMapping from './mapping';
|
||||||
|
|
||||||
|
import type { Emoji as EmojiMart, CustomEmoji as EmojiMartCustom } from 'soapbox/features/emoji/data';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: Consolate emoji object types
|
||||||
|
*
|
||||||
|
* There are five different emoji objects currently
|
||||||
|
* - emoji-mart's "onPickEmoji" handler
|
||||||
|
* - emoji-mart's custom emoji types
|
||||||
|
* - an Emoji type that is either NativeEmoji or CustomEmoji
|
||||||
|
* - a type inside redux's `store.custom_emoji` immutablejs
|
||||||
|
*
|
||||||
|
* there needs to be one type for the picker handler callback
|
||||||
|
* and one type for the emoji-mart data
|
||||||
|
* and one type that is used everywhere that the above two are converted into
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CustomEmoji {
|
||||||
|
id: string
|
||||||
|
colons: string
|
||||||
|
custom: true
|
||||||
|
imageUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NativeEmoji {
|
||||||
|
id: string
|
||||||
|
colons: string
|
||||||
|
custom?: boolean
|
||||||
|
unified: string
|
||||||
|
native: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Emoji = CustomEmoji | NativeEmoji;
|
||||||
|
|
||||||
|
export function isCustomEmoji(emoji: Emoji): emoji is CustomEmoji {
|
||||||
|
return (emoji as CustomEmoji).imageUrl !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNativeEmoji(emoji: Emoji): emoji is NativeEmoji {
|
||||||
|
return (emoji as NativeEmoji).native !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAlphaNumeric = (c: string) => {
|
||||||
|
const code = c.charCodeAt(0);
|
||||||
|
|
||||||
|
if (!(code > 47 && code < 58) && // numeric (0-9)
|
||||||
|
!(code > 64 && code < 91) && // upper alpha (A-Z)
|
||||||
|
!(code > 96 && code < 123)) { // lower alpha (a-z)
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validEmojiChar = (c: string) => {
|
||||||
|
return isAlphaNumeric(c)
|
||||||
|
|| c === '_'
|
||||||
|
|| c === '-'
|
||||||
|
|| c === '.';
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertCustom = (shortname: string, filename: string) => {
|
||||||
|
return `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertUnicode = (c: string) => {
|
||||||
|
const { unified, shortcode } = unicodeMapping[c];
|
||||||
|
|
||||||
|
return `<img draggable="false" class="emojione" alt="${c}" title=":${shortcode}:" src="/packs/emoji/${unified}.svg" />`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertEmoji = (str: string, customEmojis: any) => {
|
||||||
|
if (str.length < 3) return str;
|
||||||
|
if (str in customEmojis) {
|
||||||
|
const emoji = customEmojis[str];
|
||||||
|
const filename = emoji.static_url;
|
||||||
|
|
||||||
|
if (filename?.length > 0) {
|
||||||
|
return convertCustom(str, filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const emojifyText = (str: string, customEmojis = {}) => {
|
||||||
|
let buf = '';
|
||||||
|
let stack = '';
|
||||||
|
let open = false;
|
||||||
|
|
||||||
|
const clearStack = () => {
|
||||||
|
buf += stack;
|
||||||
|
open = false;
|
||||||
|
stack = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let c of split(str)) {
|
||||||
|
// convert FE0E selector to FE0F so it can be found in unimap
|
||||||
|
if (c.codePointAt(c.length - 1) === 65038) {
|
||||||
|
c = c.slice(0, -1) + String.fromCodePoint(65039);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unqualified emojis aren't in emoji-mart's mappings so we just add FEOF
|
||||||
|
const unqualified = c + String.fromCodePoint(65039);
|
||||||
|
|
||||||
|
if (c in unicodeMapping) {
|
||||||
|
if (open) { // unicode emoji inside colon
|
||||||
|
clearStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
buf += convertUnicode(c);
|
||||||
|
} else if (unqualified in unicodeMapping) {
|
||||||
|
if (open) { // unicode emoji inside colon
|
||||||
|
clearStack();
|
||||||
|
}
|
||||||
|
|
||||||
|
buf += convertUnicode(unqualified);
|
||||||
|
} else if (c === ':') {
|
||||||
|
stack += ':';
|
||||||
|
|
||||||
|
// we see another : we convert it and clear the stack buffer
|
||||||
|
if (open) {
|
||||||
|
buf += convertEmoji(stack, customEmojis);
|
||||||
|
stack = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
open = !open;
|
||||||
|
} else {
|
||||||
|
if (open) {
|
||||||
|
stack += c;
|
||||||
|
|
||||||
|
// if the stack is non-null and we see invalid chars it's a string not emoji
|
||||||
|
// so we push it to the return result and clear it
|
||||||
|
if (!validEmojiChar(c)) {
|
||||||
|
clearStack();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buf += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// never found a closing colon so it's just a raw string
|
||||||
|
if (open) {
|
||||||
|
buf += stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseHTML = (str: string): { text: boolean, data: string }[] => {
|
||||||
|
const tokens = [];
|
||||||
|
let buf = '';
|
||||||
|
let stack = '';
|
||||||
|
let open = false;
|
||||||
|
|
||||||
|
for (const c of str) {
|
||||||
|
if (c === '<') {
|
||||||
|
if (open) {
|
||||||
|
tokens.push({ text: true, data: stack });
|
||||||
|
stack = '<';
|
||||||
|
} else {
|
||||||
|
tokens.push({ text: true, data: buf });
|
||||||
|
stack = '<';
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
} else if (c === '>') {
|
||||||
|
if (open) {
|
||||||
|
open = false;
|
||||||
|
tokens.push({ text: false, data: stack + '>' });
|
||||||
|
stack = '';
|
||||||
|
buf = '';
|
||||||
|
} else {
|
||||||
|
buf += '>';
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (open) {
|
||||||
|
stack += c;
|
||||||
|
} else {
|
||||||
|
buf += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (open) {
|
||||||
|
tokens.push({ text: true, data: buf + stack });
|
||||||
|
} else if (buf !== '') {
|
||||||
|
tokens.push({ text: true, data: buf });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emojify = (str: string, customEmojis = {}) => {
|
||||||
|
return parseHTML(str)
|
||||||
|
.map(({ text, data }) => {
|
||||||
|
if (!text) return data;
|
||||||
|
if (data.length === 0 || data === ' ') return data;
|
||||||
|
|
||||||
|
return emojifyText(data, customEmojis);
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
export default emojify;
|
||||||
|
|
||||||
|
export const buildCustomEmojis = (customEmojis: any) => {
|
||||||
|
const emojis: EmojiMart<EmojiMartCustom>[] = [];
|
||||||
|
|
||||||
|
customEmojis.forEach((emoji: any) => {
|
||||||
|
const shortcode = emoji.get('shortcode');
|
||||||
|
const url = emoji.get('static_url');
|
||||||
|
const name = shortcode.replace(':', '');
|
||||||
|
|
||||||
|
emojis.push({
|
||||||
|
id: name,
|
||||||
|
name,
|
||||||
|
keywords: [name],
|
||||||
|
skins: [{ src: url }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return emojis;
|
||||||
|
};
|
|
@ -0,0 +1,111 @@
|
||||||
|
import data, { EmojiData } from './data';
|
||||||
|
|
||||||
|
const stripLeadingZeros = /^0+/;
|
||||||
|
|
||||||
|
function replaceAll(str: string, find: string, replace: string) {
|
||||||
|
return str.replace(new RegExp(find, 'g'), replace);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnicodeMap {
|
||||||
|
[s: string]: {
|
||||||
|
unified: string
|
||||||
|
shortcode: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Twemoji strips their hex codes from unicode codepoints to make it look "pretty"
|
||||||
|
* - leading 0s are removed
|
||||||
|
* - fe0f is removed unless it has 200d
|
||||||
|
* - fe0f is NOT removed for 1f441-fe0f-200d-1f5e8-fe0f even though it has a 200d
|
||||||
|
*
|
||||||
|
* this is all wrong
|
||||||
|
*/
|
||||||
|
|
||||||
|
const blacklist = {
|
||||||
|
'1f441-fe0f-200d-1f5e8-fe0f': true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tweaks = {
|
||||||
|
'#⃣': ['23-20e3', 'hash'],
|
||||||
|
'*⃣': ['2a-20e3', 'keycap_star'],
|
||||||
|
'0⃣': ['30-20e3', 'zero'],
|
||||||
|
'1⃣': ['31-20e3', 'one'],
|
||||||
|
'2⃣': ['32-20e3', 'two'],
|
||||||
|
'3⃣': ['33-20e3', 'three'],
|
||||||
|
'4⃣': ['34-20e3', 'four'],
|
||||||
|
'5⃣': ['35-20e3', 'five'],
|
||||||
|
'6⃣': ['36-20e3', 'six'],
|
||||||
|
'7⃣': ['37-20e3', 'seven'],
|
||||||
|
'8⃣': ['38-20e3', 'eight'],
|
||||||
|
'9⃣': ['39-20e3', 'nine'],
|
||||||
|
'❤🔥': ['2764-fe0f-200d-1f525', 'heart_on_fire'],
|
||||||
|
'❤🩹': ['2764-fe0f-200d-1fa79', 'mending_heart'],
|
||||||
|
'👁🗨️': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'],
|
||||||
|
'👁️🗨': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'],
|
||||||
|
'👁🗨': ['1f441-fe0f-200d-1f5e8-fe0f', 'eye-in-speech-bubble'],
|
||||||
|
'🕵♂️': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'],
|
||||||
|
'🕵️♂': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'],
|
||||||
|
'🕵♂': ['1f575-fe0f-200d-2642-fe0f', 'male-detective'],
|
||||||
|
'🕵♀️': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'],
|
||||||
|
'🕵️♀': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'],
|
||||||
|
'🕵♀': ['1f575-fe0f-200d-2640-fe0f', 'female-detective'],
|
||||||
|
'🏌♂️': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'],
|
||||||
|
'🏌️♂': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'],
|
||||||
|
'🏌♂': ['1f3cc-fe0f-200d-2642-fe0f', 'man-golfing'],
|
||||||
|
'🏌♀️': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'],
|
||||||
|
'🏌️♀': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'],
|
||||||
|
'🏌♀': ['1f3cc-fe0f-200d-2640-fe0f', 'woman-golfing'],
|
||||||
|
'⛹♂️': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'],
|
||||||
|
'⛹️♂': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'],
|
||||||
|
'⛹♂': ['26f9-fe0f-200d-2642-fe0f', 'man-bouncing-ball'],
|
||||||
|
'⛹♀️': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'],
|
||||||
|
'⛹️♀': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'],
|
||||||
|
'⛹♀': ['26f9-fe0f-200d-2640-fe0f', 'woman-bouncing-ball'],
|
||||||
|
'🏋♂️': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'],
|
||||||
|
'🏋️♂': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'],
|
||||||
|
'🏋♂': ['1f3cb-fe0f-200d-2642-fe0f', 'man-lifting-weights'],
|
||||||
|
'🏋♀️': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'],
|
||||||
|
'🏋️♀': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'],
|
||||||
|
'🏋♀': ['1f3cb-fe0f-200d-2640-fe0f', 'woman-lifting-weights'],
|
||||||
|
'🏳🌈': ['1f3f3-fe0f-200d-1f308', 'rainbow_flag'],
|
||||||
|
'🏳⚧️': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'],
|
||||||
|
'🏳️⚧': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'],
|
||||||
|
'🏳⚧': ['1f3f3-fe0f-200d-26a7-fe0f', 'transgender_flag'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripcodes = (unified: string, native: string) => {
|
||||||
|
const stripped = unified.replace(stripLeadingZeros, '');
|
||||||
|
|
||||||
|
if (unified.includes('200d') && !(unified in blacklist)) {
|
||||||
|
return stripped;
|
||||||
|
} else {
|
||||||
|
return replaceAll(stripped, '-fe0f', '');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateMappings = (data: EmojiData): UnicodeMap => {
|
||||||
|
const result: UnicodeMap = {};
|
||||||
|
const emojis = Object.values(data.emojis ?? {});
|
||||||
|
|
||||||
|
for (const value of emojis) {
|
||||||
|
for (const item of value.skins) {
|
||||||
|
const { unified, native } = item;
|
||||||
|
const stripped = stripcodes(unified, native);
|
||||||
|
|
||||||
|
result[native] = { unified: stripped, shortcode: value.id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [native, [unified, shortcode]] of Object.entries(tweaks)) {
|
||||||
|
const stripped = stripcodes(unified, native);
|
||||||
|
|
||||||
|
result[native] = { unified: stripped, shortcode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const unicodeMapping = generateMappings(data);
|
||||||
|
|
||||||
|
export default unicodeMapping;
|
|
@ -0,0 +1,65 @@
|
||||||
|
import { Index } from 'flexsearch';
|
||||||
|
|
||||||
|
import data from './data';
|
||||||
|
|
||||||
|
import type { Emoji } from './index';
|
||||||
|
// import type { Emoji as EmojiMart, CustomEmoji } from 'emoji-mart';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const index = new Index({
|
||||||
|
tokenize: 'full',
|
||||||
|
optimize: true,
|
||||||
|
context: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [key, emoji] of Object.entries(data.emojis)) {
|
||||||
|
index.add('n' + key, emoji.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface searchOptions {
|
||||||
|
maxResults?: number
|
||||||
|
custom?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addCustomToPool = (customEmojis: any[]) => {
|
||||||
|
// @ts-ignore
|
||||||
|
for (const key in index.register) {
|
||||||
|
if (key[0] === 'c') {
|
||||||
|
index.remove(key); // remove old custom emojis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
for (const emoji of customEmojis) {
|
||||||
|
index.add('c' + i++, emoji.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// we can share an index by prefixing custom emojis with 'c' and native with 'n'
|
||||||
|
const search = (str: string, { maxResults = 5, custom }: searchOptions = {}, custom_emojis?: any): Emoji[] => {
|
||||||
|
return index.search(str, maxResults)
|
||||||
|
.flatMap((id: string) => {
|
||||||
|
if (id[0] === 'c') {
|
||||||
|
const { shortcode, static_url } = custom_emojis.get((id as string).slice(1)).toJS();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: shortcode,
|
||||||
|
colons: ':' + shortcode + ':',
|
||||||
|
custom: true,
|
||||||
|
imageUrl: static_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { skins } = data.emojis[(id as string).slice(1)];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: (id as string).slice(1),
|
||||||
|
colons: ':' + id.slice(1) + ':',
|
||||||
|
unified: skins[0].unified,
|
||||||
|
native: skins[0].native,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default search;
|
|
@ -1,26 +0,0 @@
|
||||||
// taken from:
|
|
||||||
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
|
|
||||||
exports.unicodeToFilename = (str) => {
|
|
||||||
let result = '';
|
|
||||||
let charCode = 0;
|
|
||||||
let p = 0;
|
|
||||||
let i = 0;
|
|
||||||
while (i < str.length) {
|
|
||||||
charCode = str.charCodeAt(i++);
|
|
||||||
if (p) {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16);
|
|
||||||
p = 0;
|
|
||||||
} else if (0xD800 <= charCode && charCode <= 0xDBFF) {
|
|
||||||
p = charCode;
|
|
||||||
} else {
|
|
||||||
if (result.length > 0) {
|
|
||||||
result += '-';
|
|
||||||
}
|
|
||||||
result += charCode.toString(16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
|
@ -1,21 +0,0 @@
|
||||||
function padLeft(str, num) {
|
|
||||||
while (str.length < num) {
|
|
||||||
str = '0' + str;
|
|
||||||
}
|
|
||||||
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.unicodeToUnifiedName = (str) => {
|
|
||||||
let output = '';
|
|
||||||
|
|
||||||
for (let i = 0; i < str.length; i += 2) {
|
|
||||||
if (i > 0) {
|
|
||||||
output += '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return output;
|
|
||||||
};
|
|
|
@ -13,10 +13,10 @@ const messages = defineMessages({
|
||||||
|
|
||||||
interface IIconPickerDropdown {
|
interface IIconPickerDropdown {
|
||||||
value: string
|
value: string
|
||||||
onPickEmoji: React.ChangeEventHandler
|
onPickIcon: (icon: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconPickerDropdown: React.FC<IIconPickerDropdown> = ({ value, onPickEmoji }) => {
|
const IconPickerDropdown: React.FC<IIconPickerDropdown> = ({ value, onPickIcon }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const [active, setActive] = useState(false);
|
const [active, setActive] = useState(false);
|
||||||
|
@ -73,9 +73,9 @@ const IconPickerDropdown: React.FC<IIconPickerDropdown> = ({ value, onPickEmoji
|
||||||
|
|
||||||
<Overlay show={active} placement={placement} target={target.current}>
|
<Overlay show={active} placement={placement} target={target.current}>
|
||||||
<IconPickerMenu
|
<IconPickerMenu
|
||||||
customEmojis={forkAwesomeIcons}
|
icons={forkAwesomeIcons}
|
||||||
onClose={onHideDropdown}
|
onClose={onHideDropdown}
|
||||||
onPick={onPickEmoji}
|
onPick={onPickIcon}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,31 +1,25 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
// @ts-ignore
|
|
||||||
import Picker from 'emoji-mart/dist-es/components/picker/picker';
|
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
emoji: { id: 'icon_button.label', defaultMessage: 'Select icon' },
|
||||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
|
||||||
emoji_not_found: { id: 'icon_button.not_found', defaultMessage: 'No icons!! (╯°□°)╯︵ ┻━┻' },
|
|
||||||
custom: { id: 'icon_button.icons', defaultMessage: 'Icons' },
|
|
||||||
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const backgroundImageFn = () => '';
|
|
||||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||||
|
|
||||||
const categoriesSort = ['custom'];
|
|
||||||
|
|
||||||
interface IIconPickerMenu {
|
interface IIconPickerMenu {
|
||||||
customEmojis: Record<string, Array<string>>
|
icons: Record<string, Array<string>>
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onPick: any
|
onPick: (icon: string) => void
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ customEmojis, onClose, onPick, style }) => {
|
const IconPickerMenu: React.FC<IIconPickerMenu> = ({ icons, onClose, onPick, style }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const node = useRef<HTMLDivElement | null>(null);
|
const node = useRef<HTMLDivElement | null>(null);
|
||||||
|
@ -60,70 +54,42 @@ const IconPickerMenu: React.FC<IIconPickerMenu> = ({ customEmojis, onClose, onPi
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getI18n = () => {
|
const handleClick = (icon: string) => {
|
||||||
|
|
||||||
return {
|
|
||||||
search: intl.formatMessage(messages.emoji_search),
|
|
||||||
notfound: intl.formatMessage(messages.emoji_not_found),
|
|
||||||
categories: {
|
|
||||||
search: intl.formatMessage(messages.search_results),
|
|
||||||
custom: intl.formatMessage(messages.custom),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = (emoji: Record<string, any>) => {
|
|
||||||
emoji.native = emoji.colons;
|
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
onPick(emoji);
|
onPick(icon);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildIcons = () => {
|
const renderIcon = (icon: string) => {
|
||||||
const emojis: Record<string, any> = [];
|
|
||||||
|
|
||||||
Object.values(customEmojis).forEach((category) => {
|
|
||||||
category.forEach((icon) => {
|
|
||||||
const name = icon.replace('fa fa-', '');
|
const name = icon.replace('fa fa-', '');
|
||||||
if (icon !== 'email' && icon !== 'memo') {
|
|
||||||
emojis.push({
|
|
||||||
id: name,
|
|
||||||
name,
|
|
||||||
short_names: [name],
|
|
||||||
emoticons: [],
|
|
||||||
keywords: [name],
|
|
||||||
imageUrl: '',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return emojis;
|
return (
|
||||||
|
<li key={icon} className='col-span-1 inline-block'>
|
||||||
|
<button
|
||||||
|
className='flex items-center justify-center rounded-full p-1.5 hover:bg-gray-50 dark:hover:bg-primary-800'
|
||||||
|
aria-label={name}
|
||||||
|
title={name}
|
||||||
|
onClick={() => handleClick(name)}
|
||||||
|
>
|
||||||
|
<i className={clsx(icon, 'h-[1.375rem] w-[1.375rem] text-lg leading-[1.15]')} />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = { compressed: true, categories: [], aliases: [], emojis: [] };
|
|
||||||
const title = intl.formatMessage(messages.emoji);
|
const title = intl.formatMessage(messages.emoji);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={clsx('font-icon-picker emoji-picker-dropdown__menu')} style={style} ref={setRef}>
|
<div
|
||||||
<Picker
|
className={clsx('absolute z-[101] -my-0.5')}
|
||||||
perLine={8}
|
style={{ transform: 'translateX(calc(-1 * env(safe-area-inset-right)))', ...style }}
|
||||||
emojiSize={22}
|
ref={setRef}
|
||||||
include={categoriesSort}
|
>
|
||||||
sheetSize={32}
|
<div className='h-[270px] overflow-x-hidden overflow-y-scroll rounded bg-white p-1.5 text-gray-900 dark:bg-primary-900 dark:text-gray-100' aria-label={title}>
|
||||||
custom={buildIcons()}
|
<Text className='px-1.5 py-1'><FormattedMessage id='icon_button.icons' defaultMessage='Icons' /></Text>
|
||||||
color=''
|
<ul className='grid grid-cols-8'>
|
||||||
emoji=''
|
{Object.values(icons).flat().map(icon => renderIcon(icon))}
|
||||||
set=''
|
</ul>
|
||||||
title={title}
|
</div>
|
||||||
i18n={getI18n()}
|
|
||||||
onClick={handleClick}
|
|
||||||
showPreview={false}
|
|
||||||
backgroundImageFn={backgroundImageFn}
|
|
||||||
emojiTooltip
|
|
||||||
noShowAnchors
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,15 +4,13 @@ import IconPickerDropdown from './icon-picker-dropdown';
|
||||||
|
|
||||||
interface IIconPicker {
|
interface IIconPicker {
|
||||||
value: string
|
value: string
|
||||||
onChange: React.ChangeEventHandler
|
onChange: (icon: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconPicker: React.FC<IIconPicker> = ({ value, onChange }) => {
|
const IconPicker: React.FC<IIconPicker> = ({ value, onChange }) => (
|
||||||
return (
|
|
||||||
<div className='relative mt-1 rounded-md border border-solid border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-800'>
|
<div className='relative mt-1 rounded-md border border-solid border-gray-300 shadow-sm dark:border-gray-600 dark:bg-gray-800'>
|
||||||
<IconPickerDropdown value={value} onPickEmoji={onChange} />
|
<IconPickerDropdown value={value} onPickIcon={onChange} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export default IconPicker;
|
export default IconPicker;
|
||||||
|
|
|
@ -17,8 +17,8 @@ const messages = defineMessages({
|
||||||
const PromoPanelInput: StreamfieldComponent<PromoPanelItem> = ({ value, onChange }) => {
|
const PromoPanelInput: StreamfieldComponent<PromoPanelItem> = ({ value, onChange }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const handleIconChange = (icon: any) => {
|
const handleIconChange = (icon: string) => {
|
||||||
onChange(value.set('icon', icon.id));
|
onChange(value.set('icon', icon));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => {
|
const handleChange = (key: 'text' | 'url'): React.ChangeEventHandler<HTMLInputElement> => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { logOut } from 'soapbox/actions/auth';
|
import { logOut } from 'soapbox/actions/auth';
|
||||||
import { Text } from 'soapbox/components/ui';
|
import { Text } from 'soapbox/components/ui';
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { useSoapboxConfig, useOwnAccount, useFeatures, useAppDispatch } from 'soapbox/hooks';
|
import { useSoapboxConfig, useOwnAccount, useFeatures, useAppDispatch } from 'soapbox/hooks';
|
||||||
import sourceCode from 'soapbox/utils/code';
|
import sourceCode from 'soapbox/utils/code';
|
||||||
|
|
||||||
|
|
|
@ -572,7 +572,7 @@ const UI: React.FC<IUI> = ({ children }) => {
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
hotkeys.current.__mousetrap__.stopCallback = (_e, element) => {
|
hotkeys.current.__mousetrap__.stopCallback = (_e, element) => {
|
||||||
return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName);
|
return ['TEXTAREA', 'SELECT', 'INPUT', 'EM-EMOJI-PICKER'].includes(element.tagName);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export function EmojiPicker() {
|
export function EmojiPicker() {
|
||||||
return import(/* webpackChunkName: "emoji_picker" */'../../emoji/emoji-picker');
|
return import(/* webpackChunkName: "emoji_picker" */'../../emoji/components/emoji-picker');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Notifications() {
|
export function Notifications() {
|
||||||
|
|
|
@ -620,6 +620,7 @@
|
||||||
"email_verifilcation.exists": "This email has already been taken.",
|
"email_verifilcation.exists": "This email has already been taken.",
|
||||||
"embed.instructions": "Embed this post on your website by copying the code below.",
|
"embed.instructions": "Embed this post on your website by copying the code below.",
|
||||||
"emoji_button.activity": "Activity",
|
"emoji_button.activity": "Activity",
|
||||||
|
"emoji_button.add_custom": "Add custom emoji",
|
||||||
"emoji_button.custom": "Custom",
|
"emoji_button.custom": "Custom",
|
||||||
"emoji_button.flags": "Flags",
|
"emoji_button.flags": "Flags",
|
||||||
"emoji_button.food": "Food & Drink",
|
"emoji_button.food": "Food & Drink",
|
||||||
|
@ -627,10 +628,19 @@
|
||||||
"emoji_button.nature": "Nature",
|
"emoji_button.nature": "Nature",
|
||||||
"emoji_button.not_found": "No emojis found.",
|
"emoji_button.not_found": "No emojis found.",
|
||||||
"emoji_button.objects": "Objects",
|
"emoji_button.objects": "Objects",
|
||||||
|
"emoji_button.oh_no": "Oh no!",
|
||||||
"emoji_button.people": "People",
|
"emoji_button.people": "People",
|
||||||
|
"emoji_button.pick": "Pick an emoji…",
|
||||||
"emoji_button.recent": "Frequently used",
|
"emoji_button.recent": "Frequently used",
|
||||||
"emoji_button.search": "Search…",
|
"emoji_button.search": "Search…",
|
||||||
"emoji_button.search_results": "Search results",
|
"emoji_button.search_results": "Search results",
|
||||||
|
"emoji_button.skins_1": "Default",
|
||||||
|
"emoji_button.skins_2": "Light",
|
||||||
|
"emoji_button.skins_3": "Medium-Light",
|
||||||
|
"emoji_button.skins_4": "Medium",
|
||||||
|
"emoji_button.skins_5": "Medium-Dark",
|
||||||
|
"emoji_button.skins_6": "Dark",
|
||||||
|
"emoji_button.skins_choose": "Choose default skin tone",
|
||||||
"emoji_button.symbols": "Symbols",
|
"emoji_button.symbols": "Symbols",
|
||||||
"emoji_button.travel": "Travel & Places",
|
"emoji_button.travel": "Travel & Places",
|
||||||
"empty_column.account_blocked": "You are blocked by @{accountUsername}.",
|
"empty_column.account_blocked": "You are blocked by @{accountUsername}.",
|
||||||
|
@ -794,7 +804,6 @@
|
||||||
"home.column_settings.show_replies": "Show replies",
|
"home.column_settings.show_replies": "Show replies",
|
||||||
"icon_button.icons": "Icons",
|
"icon_button.icons": "Icons",
|
||||||
"icon_button.label": "Select icon",
|
"icon_button.label": "Select icon",
|
||||||
"icon_button.not_found": "No icons!! (╯°□°)╯︵ ┻━┻",
|
|
||||||
"import_data.actions.import": "Import",
|
"import_data.actions.import": "Import",
|
||||||
"import_data.actions.import_blocks": "Import blocks",
|
"import_data.actions.import_blocks": "Import blocks",
|
||||||
"import_data.actions.import_follows": "Import follows",
|
"import_data.actions.import_follows": "Import follows",
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
fromJS,
|
fromJS,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { unescapeHTML } from 'soapbox/utils/html';
|
import { unescapeHTML } from 'soapbox/utils/html';
|
||||||
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
|
import { mergeDefined, makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
fromJS,
|
fromJS,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
fromJS,
|
fromJS,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { unescapeHTML } from 'soapbox/utils/html';
|
import { unescapeHTML } from 'soapbox/utils/html';
|
||||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
fromJS,
|
fromJS,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
import { makeEmojiMap } from 'soapbox/utils/normalizers';
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
fromJS,
|
fromJS,
|
||||||
} from 'immutable';
|
} from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
import { normalizeAttachment } from 'soapbox/normalizers/attachment';
|
||||||
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
import { normalizeEmoji } from 'soapbox/normalizers/emoji';
|
||||||
import { normalizePoll } from 'soapbox/normalizers/poll';
|
import { normalizePoll } from 'soapbox/normalizers/poll';
|
||||||
|
|
|
@ -118,7 +118,7 @@ describe('statuses reducer', () => {
|
||||||
const status = require('soapbox/__fixtures__/status-custom-emoji.json');
|
const status = require('soapbox/__fixtures__/status-custom-emoji.json');
|
||||||
const action = { type: STATUS_IMPORT, status };
|
const action = { type: STATUS_IMPORT, status };
|
||||||
|
|
||||||
const expected = 'Hello <img draggable="false" class="emojione custom-emoji" alt=":ablobcathyper:" title=":ablobcathyper:" src="https://gleasonator.com/emoji/blobcat/ablobcathyper.png" data-original="https://gleasonator.com/emoji/blobcat/ablobcathyper.png" data-static="https://gleasonator.com/emoji/blobcat/ablobcathyper.png"> <img draggable="false" class="emojione custom-emoji" alt=":ageblobcat:" title=":ageblobcat:" src="https://gleasonator.com/emoji/blobcat/ageblobcat.png" data-original="https://gleasonator.com/emoji/blobcat/ageblobcat.png" data-static="https://gleasonator.com/emoji/blobcat/ageblobcat.png"> <img draggable="false" class="emojione" alt="😂" title=":joy:" src="/packs/emoji/1f602.svg"> world <img draggable="false" class="emojione" alt="😋" title=":yum:" src="/packs/emoji/1f60b.svg"> test <img draggable="false" class="emojione custom-emoji" alt=":blobcatphoto:" title=":blobcatphoto:" src="https://gleasonator.com/emoji/blobcat/blobcatphoto.png" data-original="https://gleasonator.com/emoji/blobcat/blobcatphoto.png" data-static="https://gleasonator.com/emoji/blobcat/blobcatphoto.png">';
|
const expected = 'Hello <img draggable="false" class="emojione" alt=":ablobcathyper:" title=":ablobcathyper:" src="https://gleasonator.com/emoji/blobcat/ablobcathyper.png"> <img draggable="false" class="emojione" alt=":ageblobcat:" title=":ageblobcat:" src="https://gleasonator.com/emoji/blobcat/ageblobcat.png"> <img draggable="false" class="emojione" alt="😂" title=":joy:" src="/packs/emoji/1f602.svg"> world <img draggable="false" class="emojione" alt="😋" title=":yum:" src="/packs/emoji/1f60b.svg"> test <img draggable="false" class="emojione" alt=":blobcatphoto:" title=":blobcatphoto:" src="https://gleasonator.com/emoji/blobcat/blobcatphoto.png">';
|
||||||
|
|
||||||
const result = reducer(undefined, action).getIn(['AGm7uC9DaAIGUa4KYK', 'contentHtml']);
|
const result = reducer(undefined, action).getIn(['AGm7uC9DaAIGUa4KYK', 'contentHtml']);
|
||||||
expect(result).toBe(expected);
|
expect(result).toBe(expected);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { isNativeEmoji } from 'soapbox/features/emoji';
|
||||||
import { tagHistory } from 'soapbox/settings';
|
import { tagHistory } from 'soapbox/settings';
|
||||||
import { PLEROMA } from 'soapbox/utils/features';
|
import { PLEROMA } from 'soapbox/utils/features';
|
||||||
import { hasIntegerMediaIds } from 'soapbox/utils/status';
|
import { hasIntegerMediaIds } from 'soapbox/utils/status';
|
||||||
|
@ -58,7 +59,7 @@ import { normalizeAttachment } from '../normalizers/attachment';
|
||||||
import { unescapeHTML } from '../utils/html';
|
import { unescapeHTML } from '../utils/html';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
import type {
|
import type {
|
||||||
Account as AccountEntity,
|
Account as AccountEntity,
|
||||||
APIEntity,
|
APIEntity,
|
||||||
|
@ -192,7 +193,8 @@ const updateSuggestionTags = (compose: Compose, token: string, currentTrends: Im
|
||||||
|
|
||||||
const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needsSpace: boolean) => {
|
const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needsSpace: boolean) => {
|
||||||
const oldText = compose.text;
|
const oldText = compose.text;
|
||||||
const emoji = needsSpace ? ' ' + emojiData.native : emojiData.native;
|
const emojiText = isNativeEmoji(emojiData) ? emojiData.native : emojiData.colons;
|
||||||
|
const emoji = needsSpace ? ' ' + emojiText : emojiText;
|
||||||
|
|
||||||
return compose.merge({
|
return compose.merge({
|
||||||
text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
|
text: `${oldText.slice(0, position)}${emoji} ${oldText.slice(position)}`,
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
|
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
|
||||||
import { emojis as emojiData } from 'soapbox/features/emoji/emoji-mart-data-light';
|
import { buildCustomEmojis } from 'soapbox/features/emoji';
|
||||||
import { addCustomToPool } from 'soapbox/features/emoji/emoji-mart-search-light';
|
import emojiData from 'soapbox/features/emoji/data';
|
||||||
|
import { addCustomToPool } from 'soapbox/features/emoji/search';
|
||||||
|
|
||||||
import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom-emojis';
|
import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom-emojis';
|
||||||
import { buildCustomEmojis } from '../features/emoji/emoji';
|
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const initialState = ImmutableList();
|
const initialState = ImmutableList<ImmutableMap<string, string>>();
|
||||||
|
|
||||||
// Populate custom emojis for composer autosuggest
|
// Populate custom emojis for composer autosuggest
|
||||||
const autosuggestPopulate = (emojis: ImmutableList<ImmutableMap<string, string>>) => {
|
const autosuggestPopulate = (emojis: ImmutableList<ImmutableMap<string, string>>) => {
|
||||||
|
@ -22,7 +22,7 @@ const importEmojis = (customEmojis: APIEntity[]) => {
|
||||||
// Otherwise it breaks EmojiMart.
|
// Otherwise it breaks EmojiMart.
|
||||||
// https://gitlab.com/soapbox-pub/soapbox/-/issues/610
|
// https://gitlab.com/soapbox-pub/soapbox/-/issues/610
|
||||||
const shortcode = emoji.get('shortcode', '').toLowerCase();
|
const shortcode = emoji.get('shortcode', '').toLowerCase();
|
||||||
return !emojiData[shortcode];
|
return !emojiData.emojis[shortcode];
|
||||||
});
|
});
|
||||||
|
|
||||||
autosuggestPopulate(emojis);
|
autosuggestPopulate(emojis);
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
FE_NAME,
|
FE_NAME,
|
||||||
} from '../actions/settings';
|
} from '../actions/settings';
|
||||||
|
|
||||||
import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
import type { Emoji } from 'soapbox/features/emoji';
|
||||||
import type { APIEntity } from 'soapbox/types/entities';
|
import type { APIEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
type State = ImmutableMap<string, any>;
|
type State = ImmutableMap<string, any>;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import emojify from 'soapbox/features/emoji/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
import { normalizeStatus } from 'soapbox/normalizers';
|
import { normalizeStatus } from 'soapbox/normalizers';
|
||||||
import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji-reacts';
|
import { simulateEmojiReact, simulateUnEmojiReact } from 'soapbox/utils/emoji-reacts';
|
||||||
import { stripCompatibilityFeatures, unescapeHTML } from 'soapbox/utils/html';
|
import { stripCompatibilityFeatures, unescapeHTML } from 'soapbox/utils/html';
|
||||||
|
|
|
@ -1,296 +1,10 @@
|
||||||
.emoji-mart,
|
em-emoji-picker {
|
||||||
.emoji-mart * {
|
--rgb-background: 255 255 255;
|
||||||
box-sizing: border-box;
|
--rgb-accent: var(--color-primary-600);
|
||||||
line-height: 1.15;
|
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji-mart {
|
.dark em-emoji-picker {
|
||||||
@apply text-base inline-block text-gray-900 dark:text-gray-100 rounded bg-white dark:bg-primary-900 shadow-lg;
|
--rgb-background: var(--color-primary-900);
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart .emoji-mart-emoji {
|
|
||||||
@apply p-1.5 align-middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-bar {
|
|
||||||
@apply border-0 border-solid border-gray-200 dark:border-gray-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-bar:first-child {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-top-left-radius: 5px;
|
|
||||||
border-top-right-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-bar:last-child {
|
|
||||||
border-top-width: 1px;
|
|
||||||
border-bottom-left-radius: 5px;
|
|
||||||
border-bottom-right-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchors {
|
|
||||||
@apply flex flex-row justify-between px-1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchor {
|
|
||||||
@apply relative block flex-auto text-gray-700 dark:text-gray-600 text-center overflow-hidden transition-colors py-3 px-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchor:focus { outline: 0; }
|
|
||||||
|
|
||||||
.emoji-mart-anchor:hover,
|
|
||||||
.emoji-mart-anchor:focus,
|
|
||||||
.emoji-mart-anchor-selected {
|
|
||||||
@apply text-gray-600 dark:text-gray-300;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchor-selected .emoji-mart-anchor-bar {
|
|
||||||
@apply bottom-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchor-bar {
|
|
||||||
@apply absolute -bottom-0.5 left-0 w-11/12 h-0.5 bg-primary-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchors i {
|
|
||||||
@apply inline-block w-full;
|
|
||||||
max-width: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-anchors svg,
|
|
||||||
.emoji-mart-anchors img {
|
|
||||||
fill: currentcolor;
|
|
||||||
height: 18px;
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-scroll {
|
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
height: 270px;
|
|
||||||
padding: 0 6px 6px;
|
|
||||||
will-change: transform; /* avoids "repaints on scroll" in mobile Chrome */
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search {
|
|
||||||
@apply relative mt-1.5 p-2.5 pr-12 bg-white dark:bg-primary-900;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search input {
|
|
||||||
@apply text-sm pr-9 block w-full border-gray-300 dark:bg-transparent dark:border-gray-800 rounded-full focus:ring-primary-500 focus:border-primary-500;
|
|
||||||
|
|
||||||
&::-moz-focus-inner {
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-search-cancel-button {
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-moz-focus-inner,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
outline: 0 !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search input,
|
|
||||||
.emoji-mart-search input::-webkit-search-decoration,
|
|
||||||
.emoji-mart-search input::-webkit-search-cancel-button,
|
|
||||||
.emoji-mart-search input::-webkit-search-results-button,
|
|
||||||
.emoji-mart-search input::-webkit-search-results-decoration {
|
|
||||||
/* remove webkit/blink styles for <input type="search">
|
|
||||||
* via https://stackoverflow.com/a/9422689 */
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search-icon {
|
|
||||||
@apply absolute z-10 border-0;
|
|
||||||
top: 20px;
|
|
||||||
right: 56px;
|
|
||||||
padding: 2px 5px 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search-icon svg {
|
|
||||||
@apply fill-gray-700 dark:fill-gray-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search-icon:hover svg {
|
|
||||||
@apply stroke-gray-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category .emoji-mart-emoji span {
|
|
||||||
@apply relative text-center;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category .emoji-mart-emoji:hover::before {
|
|
||||||
@apply bg-gray-50 dark:bg-primary-800;
|
|
||||||
z-index: 0;
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border-radius: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category-label {
|
|
||||||
z-index: 2;
|
|
||||||
position: relative;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category-label span {
|
|
||||||
@apply bg-white dark:bg-primary-900;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
font-weight: 500;
|
|
||||||
padding: 5px 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category-list {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category-list li {
|
|
||||||
list-style: none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-emoji {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
font-size: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-emoji-native {
|
|
||||||
font-family: 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI', 'Apple Color Emoji', 'Twemoji Mozilla', 'Noto Color Emoji', 'Android Emoji', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-no-results {
|
|
||||||
@apply text-sm text-center text-gray-600 dark:text-gray-300;
|
|
||||||
padding-top: 70px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-no-results-img {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-no-results .emoji-mart-category-label {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-no-results .emoji-mart-no-results-label {
|
|
||||||
margin-top: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-no-results .emoji-mart-emoji:hover::before {
|
|
||||||
content: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-preview {
|
|
||||||
@apply hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* For screenreaders only, via https://stackoverflow.com/a/19758620 */
|
|
||||||
.emoji-mart-sr-only {
|
|
||||||
position: absolute;
|
|
||||||
width: 1px;
|
|
||||||
height: 1px;
|
|
||||||
padding: 0;
|
|
||||||
margin: -1px;
|
|
||||||
overflow: hidden;
|
|
||||||
clip: rect(0, 0, 0, 0);
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker-dropdown__menu {
|
|
||||||
@apply rounded-lg absolute mt-1.5;
|
|
||||||
transform: translateX(calc(-1 * env(safe-area-inset-right))); /* iOS PWA */
|
|
||||||
z-index: 20000;
|
|
||||||
|
|
||||||
.emoji-mart-scroll {
|
|
||||||
transition: opacity 200ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selecting .emoji-mart-scroll {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker-dropdown__modifiers {
|
|
||||||
position: absolute;
|
|
||||||
top: 65px;
|
|
||||||
right: 14px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-picker-dropdown__modifiers__menu {
|
|
||||||
@apply absolute bg-white dark:bg-primary-900 rounded-3xl shadow overflow-hidden;
|
|
||||||
z-index: 4;
|
|
||||||
top: -4px;
|
|
||||||
left: -8px;
|
|
||||||
|
|
||||||
button {
|
|
||||||
@apply block cursor-pointer border-0 px-2 py-1 bg-transparent;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus,
|
|
||||||
&:active {
|
|
||||||
@apply bg-gray-300 dark:bg-primary-600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-emoji {
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.font-icon-picker {
|
|
||||||
.emoji-mart-search {
|
|
||||||
// Search doesn't work. Hide it for now.
|
|
||||||
display: none;
|
|
||||||
padding: 10px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-category-label > span {
|
|
||||||
padding: 9px 6px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-scroll {
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-search-icon {
|
|
||||||
right: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji-mart-bar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fa-hack {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
&__link {
|
&__link {
|
||||||
@apply px-2 py-2.5 space-y-1 flex flex-col flex-1 items-center text-gray-600 text-lg;
|
@apply px-2 py-2.5 space-y-1 flex flex-col flex-1 items-center text-gray-600 text-lg;
|
||||||
|
|
||||||
// padding: 8px 10px;
|
// padding: 8px 10px;
|
||||||
// display: flex;
|
// display: flex;
|
||||||
// flex-direction: column;
|
// flex-direction: column;
|
||||||
|
|
|
@ -22,7 +22,6 @@ module.exports = {
|
||||||
'app/soapbox/**/*.mjs',
|
'app/soapbox/**/*.mjs',
|
||||||
'app/soapbox/**/*.ts',
|
'app/soapbox/**/*.ts',
|
||||||
'app/soapbox/**/*.tsx',
|
'app/soapbox/**/*.tsx',
|
||||||
'!app/soapbox/features/emoji/emoji-compressed.js',
|
|
||||||
'!app/soapbox/service-worker/entry.ts',
|
'!app/soapbox/service-worker/entry.ts',
|
||||||
'!app/soapbox/jest/test-setup.ts',
|
'!app/soapbox/jest/test-setup.ts',
|
||||||
'!app/soapbox/jest/test-helpers.ts',
|
'!app/soapbox/jest/test-helpers.ts',
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
"@babel/preset-typescript": "^7.18.6",
|
||||||
"@babel/runtime": "^7.20.13",
|
"@babel/runtime": "^7.20.13",
|
||||||
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@floating-ui/react": "^0.19.1",
|
"@floating-ui/react": "^0.19.1",
|
||||||
"@fontsource/inter": "^4.5.1",
|
"@fontsource/inter": "^4.5.1",
|
||||||
"@fontsource/roboto-mono": "^4.5.8",
|
"@fontsource/roboto-mono": "^4.5.8",
|
||||||
|
@ -72,6 +73,7 @@
|
||||||
"@tanstack/react-query": "^4.0.10",
|
"@tanstack/react-query": "^4.0.10",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@types/escape-html": "^1.0.1",
|
"@types/escape-html": "^1.0.1",
|
||||||
|
"@types/flexsearch": "^0.7.3",
|
||||||
"@types/http-link-header": "^1.0.3",
|
"@types/http-link-header": "^1.0.3",
|
||||||
"@types/jest": "^29.0.0",
|
"@types/jest": "^29.0.0",
|
||||||
"@types/leaflet": "^1.8.0",
|
"@types/leaflet": "^1.8.0",
|
||||||
|
@ -104,7 +106,6 @@
|
||||||
"bootstrap-icons": "^1.5.0",
|
"bootstrap-icons": "^1.5.0",
|
||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"browserslist": "^4.16.6",
|
"browserslist": "^4.16.6",
|
||||||
"cheerio": "^1.0.0-rc.10",
|
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"core-js": "^3.27.2",
|
"core-js": "^3.27.2",
|
||||||
|
@ -113,11 +114,13 @@
|
||||||
"cssnano": "^5.1.10",
|
"cssnano": "^5.1.10",
|
||||||
"detect-passive-events": "^2.0.0",
|
"detect-passive-events": "^2.0.0",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"emoji-datasource": "5.0.1",
|
"emoji-datasource": "14.0.0",
|
||||||
"emoji-mart": "npm:emoji-mart-lazyload",
|
"emoji-mart": "^5.5.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"exif-js": "^2.3.0",
|
"exif-js": "^2.3.0",
|
||||||
|
"flexsearch": "^0.7.31",
|
||||||
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
||||||
|
"graphemesplit": "^2.4.4",
|
||||||
"html-webpack-harddisk-plugin": "^2.0.0",
|
"html-webpack-harddisk-plugin": "^2.0.0",
|
||||||
"html-webpack-plugin": "^5.5.0",
|
"html-webpack-plugin": "^5.5.0",
|
||||||
"http-link-header": "^1.0.2",
|
"http-link-header": "^1.0.2",
|
||||||
|
|
112
yarn.lock
112
yarn.lock
|
@ -1698,6 +1698,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70"
|
||||||
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==
|
||||||
|
|
||||||
|
"@emoji-mart/data@^1.1.2":
|
||||||
|
version "1.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@emoji-mart/data/-/data-1.1.2.tgz#777c976f8f143df47cbb23a7077c9ca9fe5fc513"
|
||||||
|
integrity sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==
|
||||||
|
|
||||||
"@es-joy/jsdoccomment@~0.36.0":
|
"@es-joy/jsdoccomment@~0.36.0":
|
||||||
version "0.36.0"
|
version "0.36.0"
|
||||||
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.0.tgz#e3898aad334281a10ceb3c0ec406297a79f2b043"
|
resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.0.tgz#e3898aad334281a10ceb3c0ec406297a79f2b043"
|
||||||
|
@ -4128,6 +4133,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee"
|
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.29.tgz#a48795ecadf957f6c0d10e0c34af86c098fa5bee"
|
||||||
integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==
|
integrity sha512-BsPXH/irW0ht0Ji6iw/jJaK8Lj3FJemon2gvEqHKpCdDCeemHa+rI3WBGq5z7cDMZgoLjY40oninGxqk+8NzNQ==
|
||||||
|
|
||||||
|
"@types/flexsearch@^0.7.3":
|
||||||
|
version "0.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/flexsearch/-/flexsearch-0.7.3.tgz#ee79b1618035c82284278e05652e91116765b634"
|
||||||
|
integrity sha512-HXwADeHEP4exXkCIwy2n1+i0f1ilP1ETQOH5KDOugjkTFZPntWo0Gr8stZOaebkxsdx+k0X/K6obU/+it07ocg==
|
||||||
|
|
||||||
"@types/fs-extra@^9.0.1":
|
"@types/fs-extra@^9.0.1":
|
||||||
version "9.0.13"
|
version "9.0.13"
|
||||||
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
|
resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.13.tgz#7594fbae04fe7f1918ce8b3d213f74ff44ac1f45"
|
||||||
|
@ -6577,30 +6587,6 @@ character-reference-invalid@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560"
|
||||||
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==
|
||||||
|
|
||||||
cheerio-select@^1.5.0:
|
|
||||||
version "1.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-1.5.0.tgz#faf3daeb31b17c5e1a9dabcee288aaf8aafa5823"
|
|
||||||
integrity sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==
|
|
||||||
dependencies:
|
|
||||||
css-select "^4.1.3"
|
|
||||||
css-what "^5.0.1"
|
|
||||||
domelementtype "^2.2.0"
|
|
||||||
domhandler "^4.2.0"
|
|
||||||
domutils "^2.7.0"
|
|
||||||
|
|
||||||
cheerio@^1.0.0-rc.10:
|
|
||||||
version "1.0.0-rc.10"
|
|
||||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.10.tgz#2ba3dcdfcc26e7956fc1f440e61d51c643379f3e"
|
|
||||||
integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==
|
|
||||||
dependencies:
|
|
||||||
cheerio-select "^1.5.0"
|
|
||||||
dom-serializer "^1.3.2"
|
|
||||||
domhandler "^4.2.0"
|
|
||||||
htmlparser2 "^6.1.0"
|
|
||||||
parse5 "^6.0.1"
|
|
||||||
parse5-htmlparser2-tree-adapter "^6.0.1"
|
|
||||||
tslib "^2.2.0"
|
|
||||||
|
|
||||||
"chokidar@>=3.0.0 <4.0.0":
|
"chokidar@>=3.0.0 <4.0.0":
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
|
||||||
|
@ -7315,7 +7301,7 @@ css-tree@^1.1.2, css-tree@^1.1.3:
|
||||||
mdn-data "2.0.14"
|
mdn-data "2.0.14"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
css-what@^5.0.0, css-what@^5.0.1:
|
css-what@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
|
resolved "https://registry.yarnpkg.com/css-what/-/css-what-5.0.1.tgz#3efa820131f4669a8ac2408f9c32e7c7de9f4cad"
|
||||||
integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
|
integrity sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==
|
||||||
|
@ -7838,7 +7824,7 @@ dom-helpers@^3.2.1, dom-helpers@^3.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.1.2"
|
"@babel/runtime" "^7.1.2"
|
||||||
|
|
||||||
dom-serializer@^1.0.1, dom-serializer@^1.3.2:
|
dom-serializer@^1.0.1:
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
|
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
|
||||||
integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
|
integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==
|
||||||
|
@ -7883,7 +7869,7 @@ domhandler@^4.0.0, domhandler@^4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
domelementtype "^2.2.0"
|
domelementtype "^2.2.0"
|
||||||
|
|
||||||
domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0:
|
domutils@^2.5.2, domutils@^2.6.0:
|
||||||
version "2.8.0"
|
version "2.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
|
||||||
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
|
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
|
||||||
|
@ -7995,19 +7981,15 @@ emittery@^0.13.1:
|
||||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
|
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
|
||||||
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
|
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
|
||||||
|
|
||||||
emoji-datasource@5.0.1:
|
emoji-datasource@14.0.0:
|
||||||
version "5.0.1"
|
version "14.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-5.0.1.tgz#31eaaff7caa6640929327b4f4ff66f2bf313df0a"
|
resolved "https://registry.yarnpkg.com/emoji-datasource/-/emoji-datasource-14.0.0.tgz#99529a62f3a86546fc670c09b672ddc9f24f3d44"
|
||||||
integrity sha512-RXokuCv4o8RFLiigN1skAdZwJuJWqtBvcK3GVKpvAL/7BeH95enmKsli7cG8YZ85RTjyEe3+GAdpJJOV43KLKQ==
|
integrity sha512-SoOv0lSa+9/2X9ulSRDhu2u1zAOaOv5vtMY3OYUDcQCoReEh0/3eQAMuBM9LyD7Hy3G4K7mDPDqVeHUWvy7cow==
|
||||||
|
|
||||||
"emoji-mart@npm:emoji-mart-lazyload":
|
emoji-mart@^5.5.2:
|
||||||
version "3.0.1-j"
|
version "5.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-mart-lazyload/-/emoji-mart-lazyload-3.0.1-j.tgz#87a90d30b79d9145ece078d53e3e683c1a10ce9c"
|
resolved "https://registry.yarnpkg.com/emoji-mart/-/emoji-mart-5.5.2.tgz#3ddbaf053139cf4aa217650078bc1c50ca8381af"
|
||||||
integrity sha512-0wKF7MR0/iAeCIoiBLY+JjXCugycTgYRC2SL0y9/bjNSQlbeMdzILmPQJAufU/mgLFDUitOvjxLDhOZ9yxZ48g==
|
integrity sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==
|
||||||
dependencies:
|
|
||||||
"@babel/runtime" "^7.0.0"
|
|
||||||
intersection-observer "^0.12.0"
|
|
||||||
prop-types "^15.6.0"
|
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
|
@ -9062,6 +9044,11 @@ flatted@^3.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
|
||||||
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
|
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
|
||||||
|
|
||||||
|
flexsearch@^0.7.31:
|
||||||
|
version "0.7.31"
|
||||||
|
resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.31.tgz#065d4110b95083110b9b6c762a71a77cc52e4702"
|
||||||
|
integrity sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA==
|
||||||
|
|
||||||
flush-write-stream@^1.0.0:
|
flush-write-stream@^1.0.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
|
resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
|
||||||
|
@ -9627,6 +9614,14 @@ grapheme-splitter@^1.0.4:
|
||||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||||
|
|
||||||
|
graphemesplit@^2.4.4:
|
||||||
|
version "2.4.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/graphemesplit/-/graphemesplit-2.4.4.tgz#6d325c61e928efdaec2189f54a9b87babf89b75a"
|
||||||
|
integrity sha512-lKrpp1mk1NH26USxC/Asw4OHbhSQf5XfrWZ+CDv/dFVvd1j17kFgMotdJvOesmHkbFX9P9sBfpH8VogxOWLg8w==
|
||||||
|
dependencies:
|
||||||
|
js-base64 "^3.6.0"
|
||||||
|
unicode-trie "^2.0.0"
|
||||||
|
|
||||||
gzip-size@^6.0.0:
|
gzip-size@^6.0.0:
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
|
resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462"
|
||||||
|
@ -10328,11 +10323,6 @@ interpret@^3.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
|
resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4"
|
||||||
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
|
integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==
|
||||||
|
|
||||||
intersection-observer@^0.12.0:
|
|
||||||
version "0.12.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.0.tgz#6c84628f67ce8698e5f9ccf857d97718745837aa"
|
|
||||||
integrity sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ==
|
|
||||||
|
|
||||||
intersection-observer@^0.12.2:
|
intersection-observer@^0.12.2:
|
||||||
version "0.12.2"
|
version "0.12.2"
|
||||||
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375"
|
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375"
|
||||||
|
@ -11528,6 +11518,11 @@ jest@^29.0.0:
|
||||||
import-local "^3.0.2"
|
import-local "^3.0.2"
|
||||||
jest-cli "^29.3.1"
|
jest-cli "^29.3.1"
|
||||||
|
|
||||||
|
js-base64@^3.6.0:
|
||||||
|
version "3.7.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
|
||||||
|
integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==
|
||||||
|
|
||||||
js-sdsl@^4.1.4:
|
js-sdsl@^4.1.4:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0"
|
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0"
|
||||||
|
@ -13344,6 +13339,11 @@ p-try@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||||
|
|
||||||
|
pako@^0.2.5:
|
||||||
|
version "0.2.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
|
||||||
|
integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==
|
||||||
|
|
||||||
pako@~1.0.5:
|
pako@~1.0.5:
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
|
||||||
|
@ -13444,14 +13444,7 @@ parse-passwd@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
|
resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6"
|
||||||
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
|
integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@^6.0.1:
|
parse5@^6.0.0:
|
||||||
version "6.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
|
||||||
integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
|
|
||||||
dependencies:
|
|
||||||
parse5 "^6.0.1"
|
|
||||||
|
|
||||||
parse5@^6.0.0, parse5@^6.0.1:
|
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||||
|
@ -16754,6 +16747,11 @@ timers-browserify@^2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
setimmediate "^1.0.4"
|
setimmediate "^1.0.4"
|
||||||
|
|
||||||
|
tiny-inflate@^1.0.0:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
||||||
|
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
||||||
|
|
||||||
tiny-invariant@^1.0.2:
|
tiny-invariant@^1.0.2:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||||
|
@ -16965,7 +16963,7 @@ tslib@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
|
||||||
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
|
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
|
||||||
|
|
||||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.1:
|
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||||
|
@ -17159,6 +17157,14 @@ unicode-property-aliases-ecmascript@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8"
|
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz#0a36cb9a585c4f6abd51ad1deddb285c165297c8"
|
||||||
integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==
|
integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==
|
||||||
|
|
||||||
|
unicode-trie@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/unicode-trie/-/unicode-trie-2.0.0.tgz#8fd8845696e2e14a8b67d78fa9e0dd2cad62fec8"
|
||||||
|
integrity sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==
|
||||||
|
dependencies:
|
||||||
|
pako "^0.2.5"
|
||||||
|
tiny-inflate "^1.0.0"
|
||||||
|
|
||||||
unified@9.2.0:
|
unified@9.2.0:
|
||||||
version "9.2.0"
|
version "9.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
|
resolved "https://registry.yarnpkg.com/unified/-/unified-9.2.0.tgz#67a62c627c40589edebbf60f53edfd4d822027f8"
|
||||||
|
|
Loading…
Reference in New Issue