import React, { useState, useEffect, memo, useRef, useMemo, useCallback, } from '../../../lib/teact/teact'; import { withGlobal } from '../../../global'; import type { FC } from '../../../lib/teact/teact'; import type { GlobalState } from '../../../global/types'; import type { EmojiModule, EmojiRawData, EmojiData, } from '../../../util/emoji'; import { MENU_TRANSITION_DURATION, RECENT_SYMBOL_SET_ID } from '../../../config'; import { IS_TOUCH_ENV } from '../../../util/environment'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { uncompressEmoji } from '../../../util/emoji'; import fastSmoothScroll from '../../../util/fastSmoothScroll'; import { pick } from '../../../util/iteratees'; import buildClassName from '../../../util/buildClassName'; import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal'; import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useLang from '../../../hooks/useLang'; import useAppLayout from '../../../hooks/useAppLayout'; import Button from '../../ui/Button'; import Loading from '../../ui/Loading'; import EmojiCategory from './EmojiCategory'; import './EmojiPicker.scss'; type OwnProps = { className?: string; onEmojiSelect: (emoji: string, name: string) => void; }; type StateProps = Pick; type EmojiCategoryData = { id: string; name: string; emojis: string[] }; const ICONS_BY_CATEGORY: Record = { recent: 'icon-recent', people: 'icon-smile', nature: 'icon-animals', foods: 'icon-eats', activity: 'icon-sport', places: 'icon-car', objects: 'icon-lamp', symbols: 'icon-language', flags: 'icon-flag', }; const OPEN_ANIMATION_DELAY = 200; // Only a few categories are above this height. const SMOOTH_SCROLL_DISTANCE = 800; const FOCUS_MARGIN = 50; const HEADER_BUTTON_WIDTH = 42; // px. Includes margins const INTERSECTION_THROTTLE = 200; const categoryIntersections: boolean[] = []; let emojiDataPromise: Promise; let emojiRawData: EmojiRawData; let emojiData: EmojiData; const EmojiPicker: FC = ({ className, recentEmojis, onEmojiSelect, }) => { // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); // eslint-disable-next-line no-null/no-null const headerRef = useRef(null); const [categories, setCategories] = useState(); const [emojis, setEmojis] = useState(); const [activeCategoryIndex, setActiveCategoryIndex] = useState(0); const { isMobile } = useAppLayout(); const { observe: observeIntersection } = useIntersectionObserver({ rootRef: containerRef, throttleMs: INTERSECTION_THROTTLE, }, (entries) => { entries.forEach((entry) => { const { id } = entry.target as HTMLDivElement; if (!id || !id.startsWith('emoji-category-')) { return; } const index = Number(id.replace('emoji-category-', '')); categoryIntersections[index] = entry.isIntersecting; }); const intersectingWithIndexes = categoryIntersections .map((isIntersecting, index) => ({ index, isIntersecting })) .filter(({ isIntersecting }) => isIntersecting); if (!intersectingWithIndexes.length) { return; } setActiveCategoryIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index); }); useHorizontalScroll(headerRef.current, !isMobile); // Scroll header when active set updates useEffect(() => { if (!categories) { return; } const header = headerRef.current; if (!header) { return; } const newLeft = activeCategoryIndex * HEADER_BUTTON_WIDTH - header.offsetWidth / 2 + HEADER_BUTTON_WIDTH / 2; fastSmoothScrollHorizontal(header, newLeft); }, [categories, activeCategoryIndex]); const lang = useLang(); const allCategories = useMemo(() => { if (!categories) { return MEMO_EMPTY_ARRAY; } const themeCategories = [...categories]; if (recentEmojis?.length) { themeCategories.unshift({ id: RECENT_SYMBOL_SET_ID, name: lang('RecentStickers'), emojis: recentEmojis, }); } return themeCategories; }, [categories, lang, recentEmojis]); // Initialize data on first render. useEffect(() => { setTimeout(() => { const exec = () => { setCategories(emojiData.categories); setEmojis(emojiData.emojis as AllEmojis); }; if (emojiData) { exec(); } else { ensureEmojiData() .then(exec); } }, OPEN_ANIMATION_DELAY); }, []); const selectCategory = useCallback((index: number) => { setActiveCategoryIndex(index); const categoryEl = document.getElementById(`emoji-category-${index}`)!; fastSmoothScroll(containerRef.current!, categoryEl, 'start', FOCUS_MARGIN, SMOOTH_SCROLL_DISTANCE); }, []); const handleEmojiSelect = useCallback((emoji: string, name: string) => { onEmojiSelect(emoji, name); }, [onEmojiSelect]); const canRenderContents = useAsyncRendering([], MENU_TRANSITION_DURATION); function renderCategoryButton(category: EmojiCategoryData, index: number) { const icon = ICONS_BY_CATEGORY[category.id]; return icon && ( ); } const containerClassName = buildClassName('EmojiPicker', className); if (!emojis || !canRenderContents) { return (
); } return (
{allCategories.map(renderCategoryButton)}
{allCategories.map((category, i) => ( = i - 1 && activeCategoryIndex <= i + 1} onEmojiSelect={handleEmojiSelect} /> ))}
); }; async function ensureEmojiData() { if (!emojiDataPromise) { emojiDataPromise = import('emoji-data-ios/emoji-data.json'); emojiRawData = (await emojiDataPromise).default; emojiData = uncompressEmoji(emojiRawData); } return emojiDataPromise; } export default memo(withGlobal( (global): StateProps => pick(global, ['recentEmojis']), )(EmojiPicker));