Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-update-emo-25jnc5/deployments/2714
marcin mikołajczak 2023-02-25 23:30:24 +01:00
rodzic 528acb8ac5
commit 2bbbcd625e
12 zmienionych plików z 202 dodań i 202 usunięć

Wyświetl plik

@ -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 = () => ({

Wyświetl plik

@ -2,7 +2,6 @@ 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/emoji/containers/emoji-picker-dropdown-container';
import { useSettings } from 'soapbox/hooks'; import { useSettings } from 'soapbox/hooks';
@ -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>

Wyświetl plik

@ -6,8 +6,7 @@ import { usePopper } from 'react-popper';
import { changeSetting } from 'soapbox/actions/settings'; import { changeSetting } from 'soapbox/actions/settings';
import { Emoji, HStack, IconButton } from 'soapbox/components/ui'; import { Emoji, HStack, IconButton } from 'soapbox/components/ui';
import { messages } from 'soapbox/features/emoji/components/emoji-picker-dropdown'; import { getFrequentlyUsedEmojis, messages } from 'soapbox/features/emoji/components/emoji-picker-dropdown';
import { getFrequentlyUsedEmojis } from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
import { EmojiPicker as EmojiPickerAsync } from 'soapbox/features/ui/util/async-components'; import { EmojiPicker as EmojiPickerAsync } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useSettings, useSoapboxConfig } from 'soapbox/hooks';
@ -88,10 +87,14 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
}; };
const handleClickOutside = (event: MouseEvent) => { const handleClickOutside = (event: MouseEvent) => {
if ([referenceElement, popperElement, document.querySelector('em-emoji-picker')].some(el => el?.contains(event.target as Node))) { if ([referenceElement, popperElement].some(el => el?.contains(event.target as Node))) {
return; return;
} }
if (document.querySelector('em-emoji-picker')) {
return setExpanded(false);
}
if (onClose) { if (onClose) {
onClose(); onClose();
} }
@ -145,6 +148,10 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({
}; };
}; };
useEffect(() => () => {
document.body.style.overflow = '';
}, []);
useEffect(() => { useEffect(() => {
setExpanded(false); setExpanded(false);
}, [visible]); }, [visible]);

Wyświetl plik

@ -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>

Wyświetl plik

@ -1,17 +1,21 @@
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import { Map as ImmutableMap } from 'immutable';
import React, { useEffect, useState, useLayoutEffect } from 'react'; import React, { useEffect, useState, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { usePopper } from 'react-popper'; import { usePopper } from 'react-popper';
import { createSelector } from 'reselect';
import { useSettings } from 'soapbox/hooks'; import { useEmoji } from 'soapbox/actions/emojis';
import { changeSetting } from 'soapbox/actions/settings';
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
import { isMobile } from 'soapbox/is-mobile'; import { isMobile } from 'soapbox/is-mobile';
import { RootState } from 'soapbox/store';
import { buildCustomEmojis } from '../../emoji'; import { buildCustomEmojis } from '../../emoji';
import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components';
import type { EmojiPick } from 'emoji-mart'; import type { EmojiPick } from 'emoji-mart';
import type { List } from 'immutable';
import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji'; import type { Emoji, CustomEmoji, NativeEmoji } from 'soapbox/features/emoji';
let EmojiPicker: any; // load asynchronously let EmojiPicker: any; // load asynchronously
@ -43,17 +47,73 @@ export const messages = defineMessages({
skins_6: { id: 'emoji_button.skins_6', defaultMessage: 'Dark' }, skins_6: { id: 'emoji_button.skins_6', defaultMessage: 'Dark' },
}); });
// TODO: fix types export interface IEmojiPickerDropdown {
interface IEmojiPickerDropdown { onPickEmoji?: (emoji: Emoji) => void
custom_emojis: List<any> condensed?: boolean
frequentlyUsedEmojis: string[] render: React.FC<{
intl: any setPopperReference: React.Ref<HTMLButtonElement>
onPickEmoji: (emoji: Emoji) => void title?: string
onSkinTone: () => void visible?: boolean
condensed: boolean loading?: boolean
render: any handleToggle: (e: Event) => void
}>
} }
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 // Fixes render bug where popover has a delayed position update
const RenderAfter = ({ children, update }: any) => { const RenderAfter = ({ children, update }: any) => {
const [nextTick, setNextTick] = useState(false); const [nextTick, setNextTick] = useState(false);
@ -75,13 +135,17 @@ const RenderAfter = ({ children, update }: any) => {
const listenerOptions = supportsPassiveEvents ? { passive: true } : false; const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ custom_emojis, frequentlyUsedEmojis, onPickEmoji, onSkinTone, condensed, render: Render }) => { const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ onPickEmoji, condensed, render: Render }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch();
const settings = useSettings(); const settings = useSettings();
const title = intl.formatMessage(messages.emoji); const title = intl.formatMessage(messages.emoji);
const userTheme = settings.get('themeMode'); const userTheme = settings.get('themeMode');
const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto'; const theme = (userTheme === 'dark' || userTheme === 'light') ? userTheme : 'auto';
const customEmojis = useAppSelector((state) => getCustomEmojis(state));
const frequentlyUsedEmojis = useAppSelector((state) => getFrequentlyUsedEmojis(state));
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [popperReference, setPopperReference] = useState<HTMLButtonElement | null>(null); const [popperReference, setPopperReference] = useState<HTMLButtonElement | null>(null);
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null); const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
@ -108,24 +172,36 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ custom_emojis, fr
const handlePick = (emoji: EmojiPick) => { const handlePick = (emoji: EmojiPick) => {
setVisible(false); setVisible(false);
let pickedEmoji: Emoji;
if (emoji.native) { if (emoji.native) {
onPickEmoji({ pickedEmoji = {
id: emoji.id, id: emoji.id,
colons: emoji.shortcodes, colons: emoji.shortcodes,
custom: false, custom: false,
native: emoji.native, native: emoji.native,
unified: emoji.unified, unified: emoji.unified,
} as NativeEmoji); } as NativeEmoji;
} else { } else {
onPickEmoji({ pickedEmoji = {
id: emoji.id, id: emoji.id,
colons: emoji.shortcodes, colons: emoji.shortcodes,
custom: true, custom: true,
imageUrl: emoji.src, imageUrl: emoji.src,
} as CustomEmoji); } 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 = () => { const getI18n = () => {
return { return {
search: intl.formatMessage(messages.emoji_search), search: intl.formatMessage(messages.emoji_search),
@ -216,12 +292,12 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ custom_emojis, fr
<RenderAfter update={update}> <RenderAfter update={update}>
{!loading && ( {!loading && (
<EmojiPicker <EmojiPicker
custom={[{ emojis: buildCustomEmojis(custom_emojis) }]} custom={[{ emojis: buildCustomEmojis(customEmojis) }]}
title={title} title={title}
onEmojiSelect={handlePick} onEmojiSelect={handlePick}
recent={frequentlyUsedEmojis} recent={frequentlyUsedEmojis}
perLine={8} perLine={8}
skin={onSkinTone} skin={handleSkinTone}
emojiSize={22} emojiSize={22}
emojiButtonSize={34} emojiButtonSize={34}
set='twitter' set='twitter'

Wyświetl plik

@ -1,119 +0,0 @@
import classNames from 'classnames';
import { Map as ImmutableMap } from 'immutable';
import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { IconButton } from 'soapbox/components/ui';
import { useEmoji } from '../../../actions/emojis';
import { getSettings, changeSetting } from '../../../actions/settings';
import EmojiPickerDropdown from '../components/emoji-picker-dropdown';
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 => state.settings.get('frequentlyUsedEmojis', ImmutableMap()),
], emojiCounters => {
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 => 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;
}
}));
const mapStateToProps = state => ({
custom_emojis: getCustomEmojis(state),
skinTone: getSettings(state).get('skinTone'),
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
});
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
onSkinTone: skinTone => {
dispatch(changeSetting(['skinTone'], skinTone));
},
onPickEmoji: emoji => {
dispatch(useEmoji(emoji)); // eslint-disable-line react-hooks/rules-of-hooks
if (onPickEmoji) {
onPickEmoji(emoji);
}
},
});
const Container = connect(mapStateToProps, mapDispatchToProps)(EmojiPickerDropdown);
const EmojiPickerDropdownWrapper = (props) => {
return (
<Container
render={
({ setPopperReference, title, visible, loading, handleToggle }) => (
<IconButton
className={classNames({
'text-gray-400 hover:text-gray-600': true,
'pulse-loading': visible && loading,
})}
ref={setPopperReference}
src={require('@tabler/icons/mood-happy.svg')}
title={title}
aria-label={title}
aria-expanded={visible}
role='button'
onClick={handleToggle}
onKeyDown={handleToggle}
tabIndex={0}
/>
)
}
{...props}
/>
);
};
export default EmojiPickerDropdownWrapper;

Wyświetl plik

@ -0,0 +1,37 @@
import clsx from 'clsx';
import React from 'react';
import { IconButton } from 'soapbox/components/ui';
import EmojiPickerDropdown, { IEmojiPickerDropdown } from '../components/emoji-picker-dropdown';
const EmojiPickerDropdownWrapper = (props: Omit<IEmojiPickerDropdown, 'render'>) => {
return (
<EmojiPickerDropdown
render={
({ setPopperReference, title, visible, loading, handleToggle }: any) => (
<IconButton
className={clsx({
'text-gray-400 hover:text-gray-600': true,
'pulse-loading': visible && loading,
})}
ref={setPopperReference}
src={require('@tabler/icons/mood-happy.svg')}
title={title}
aria-label={title}
aria-expanded={visible}
role='button'
onClick={handleToggle}
onKeyDown={handleToggle}
tabIndex={0}
/>
)
}
{...props}
/>
);
};
export default EmojiPickerDropdownWrapper;

Wyświetl plik

@ -8,8 +8,8 @@ function replaceAll(str: string, find: string, replace: string) {
interface UnicodeMap { interface UnicodeMap {
[s: string]: { [s: string]: {
unified: string, unified: string
shortcode: string, shortcode: string
} }
} }

Wyświetl plik

@ -41,7 +41,7 @@ const search = (str: string, { maxResults = 5, custom }: searchOptions = {}, cus
.flatMap(id => { .flatMap(id => {
// @ts-ignore // @ts-ignore
if (id[0] === 'c') { if (id[0] === 'c') {
const { shortcode, static_url } = custom_emojis.get((id as string).substr(1)).toJS(); const { shortcode, static_url } = custom_emojis.get((id as string).slice(1)).toJS();
return { return {
id: shortcode, id: shortcode,
@ -51,10 +51,10 @@ const search = (str: string, { maxResults = 5, custom }: searchOptions = {}, cus
}; };
} }
const { skins } = data.emojis[(id as string).substr(1)]; const { skins } = data.emojis[(id as string).slice(1)];
return { return {
id: (id as string).substr(1), id: (id as string).slice(1),
colons: ':' + id + ':', colons: ':' + id + ':',
unified: skins[0].unified, unified: skins[0].unified,
native: skins[0].native, native: skins[0].native,

Wyświetl plik

@ -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);

Wyświetl plik

@ -9,7 +9,7 @@ import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom-emojis';
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>>) => {

Wyświetl plik

@ -1,10 +1,10 @@
declare module 'emoji-mart' { declare module 'emoji-mart' {
export interface NativeEmoji { export interface NativeEmoji {
unified: string, unified: string
native: string, native: string
x: number, x: number
y: number, y: number
} }
export interface CustomEmoji { export interface CustomEmoji {
@ -12,40 +12,40 @@ declare module 'emoji-mart' {
} }
export interface Emoji<T> { export interface Emoji<T> {
id: string, id: string
name: string, name: string
keywords: string[], keywords: string[]
skins: T[], skins: T[]
version?: number, version?: number
} }
export interface EmojiPick { export interface EmojiPick {
id: string, id: string
name: string, name: string
native?: string, native?: string
unified?: string, unified?: string
keywords: string[], keywords: string[]
shortcodes: string, shortcodes: string
emoticons: string[], emoticons: string[]
src?: string, src?: string
} }
export interface PickerProps { export interface PickerProps {
custom?: { emojis: Emoji<CustomEmoji> }[], custom?: { emojis: Emoji<CustomEmoji> }[]
set?: string, set?: string
title?: string, title?: string
theme?: string, theme?: string
onEmojiSelect?: (emoji: EmojiPick) => void, onEmojiSelect?: (emoji: EmojiPick) => void
recent?: any, recent?: any
skin?: any, skin?: any
perLine?: number, perLine?: number
emojiSize?: number, emojiSize?: number
emojiButtonSize?: number, emojiButtonSize?: number
navPosition?: string, navPosition?: string
autoFocus?: boolean, autoFocus?: boolean
i18n?: any, i18n?: any
getImageURL: (set: string, name: string) => string, getImageURL: (set: string, name: string) => string
getSpritesheetURL: (set: string) => string, getSpritesheetURL: (set: string) => string
} }
export class Picker { export class Picker {
@ -58,10 +58,10 @@ declare module 'emoji-mart' {
declare module '@emoji-mart/data/sets/14/twitter.json' { declare module '@emoji-mart/data/sets/14/twitter.json' {
export interface NativeEmoji { export interface NativeEmoji {
unified: string, unified: string
native: string, native: string
x: number, x: number
y: number, y: number
} }
export interface CustomEmoji { export interface CustomEmoji {
@ -69,36 +69,36 @@ declare module '@emoji-mart/data/sets/14/twitter.json' {
} }
export interface Emoji<T> { export interface Emoji<T> {
id: string, id: string
name: string, name: string
keywords: string[], keywords: string[]
skins: T[], skins: T[]
version?: number, version?: number
} }
export interface EmojiCategory { export interface EmojiCategory {
id: string, id: string
emojis: string[], emojis: string[]
} }
export interface EmojiMap { export interface EmojiMap {
[s: string]: Emoji<NativeEmoji>, [s: string]: Emoji<NativeEmoji>
} }
export interface EmojiAlias { export interface EmojiAlias {
[s: string]: string, [s: string]: string
} }
export interface EmojiSheet { export interface EmojiSheet {
cols: number, cols: number
rows: number, rows: number
} }
export interface EmojiData { export interface EmojiData {
categories: EmojiCategory[], categories: EmojiCategory[]
emojis: EmojiMap, emojis: EmojiMap
aliases: EmojiAlias, aliases: EmojiAlias
sheet: EmojiSheet, sheet: EmojiSheet
} }
const data: EmojiData; const data: EmojiData;