import { TextInput, Icon, Button, Divider } from '@rocket.chat/fuselage'; import { useMediaQuery, useMergedRefs, useOutsideClick } from '@rocket.chat/fuselage-hooks'; import { EmojiPickerCategoryHeader, EmojiPickerContainer, EmojiPickerPreviewArea, EmojiPickerHeader, EmojiPickerListArea, EmojiPickerPreview, } from '@rocket.chat/ui-client'; import { useTranslation, usePermission, useRoute } from '@rocket.chat/ui-contexts'; import type { ChangeEvent, KeyboardEvent, MouseEvent, RefObject } from 'react'; import { useLayoutEffect, useState, useEffect, useRef } from 'react'; import type { ListRange, VirtuosoHandle } from 'react-virtuoso'; import CategoriesResult from './CategoriesResult'; import EmojiPickerCategoryItem from './EmojiPickerCategoryItem'; import EmojiPickerDropdown from './EmojiPickerDropDown'; import SearchingResult from './SearchingResult'; import ToneSelector from './ToneSelector'; import ToneSelectorWrapper from './ToneSelector/ToneSelectorWrapper'; import { usePreviewEmoji, useEmojiPickerData } from '../../../contexts/EmojiPickerContext'; import { emoji, getCategoriesList, getEmojisBySearchTerm } from '../../../lib/emoji'; import type { EmojiItem } from '../../../lib/emoji'; import { useIsVisible } from '../../room/hooks/useIsVisible'; export type EmojiPickerProps = { reference: Element; onClose: () => void; onPickEmoji: (emoji: string) => void; }; const EmojiPicker = ({ reference, onClose, onPickEmoji }: EmojiPickerProps) => { const t = useTranslation(); const ref = useRef(reference); const virtuosoRef = useRef(null); const emojiContainerRef = useRef(null); const [isVisibleRef, isInputVisible] = useIsVisible(); const textInputRef = useRef(undefined); const mergedTextInputRef = useMergedRefs(isVisibleRef, textInputRef); const emojiCategories = getCategoriesList(); const canManageEmoji = usePermission('manage-emoji'); const customEmojiRoute = useRoute('emoji-custom'); const [searching, setSearching] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [searchResults, setSearchResults] = useState([]); const { emojiToPreview, handleRemovePreview } = usePreviewEmoji(); const { recentEmojis, setCurrentCategory, addRecentEmoji, setRecentEmojis, actualTone, currentCategory, categoriesIndexes, emojiListByCategory, customItemsLimit, setActualTone, setCustomItemsLimit, } = useEmojiPickerData(); useEffect(() => () => handleRemovePreview(), [handleRemovePreview]); const scrollCategories = useMediaQuery('(width < 340px)'); useOutsideClick([emojiContainerRef], onClose); useLayoutEffect(() => { if (!reference) { return; } const resizeObserver = new ResizeObserver(() => { const anchorRect = reference.getBoundingClientRect(); if (anchorRect.width === 0 && anchorRect.height === 0) { // The element is hidden, skip it ref.current = null; return; } ref.current = reference; }); resizeObserver.observe(reference); return () => { resizeObserver.disconnect(); }; }, [reference]); useEffect(() => { if (textInputRef.current && isInputVisible) { textInputRef.current.focus(); } }, [isInputVisible]); const handleSelectEmoji = (event: MouseEvent) => { event.stopPropagation(); const _emoji = event.currentTarget.dataset?.emoji; if (!_emoji) { return; } let tone = ''; // Custom emoji should overwrite native emoji with the same name // Custom emoji with name `:point_right:` should overwrite every tone // Custom emoji with name `:point_right_tone1:` should overwrite only tone1, and not the original or other tones if (emoji.list[`:${_emoji}:`].emojiPackage !== 'emojiCustom') { for (const emojiPackage in emoji.packages) { if (emoji.packages.hasOwnProperty(emojiPackage)) { if (actualTone > 0 && emoji.packages[emojiPackage].toneList.hasOwnProperty(_emoji)) { tone = `_tone${actualTone}`; } } } } setSearchTerm(''); onPickEmoji(_emoji + tone); addRecentEmoji(_emoji + tone); onClose(); }; useEffect(() => { setCurrentCategory('recent'); }, [setCurrentCategory]); const handleSearch = (e: ChangeEvent) => { setSearchTerm(e.target.value); setCurrentCategory(''); setSearching(e.target.value !== ''); const emojisResult = getEmojisBySearchTerm(e.target.value, actualTone, recentEmojis, setRecentEmojis); if (emojisResult.filter((emoji) => emoji.image).length === 0) { setCurrentCategory('no-results'); } setSearchResults(emojisResult as EmojiItem[]); }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); } }; const handleLoadMore = () => { setCustomItemsLimit(customItemsLimit + 90); }; const handleScroll = (range: ListRange) => { const { startIndex } = range; const category = categoriesIndexes.find( (category, index) => category.index <= startIndex + 1 && categoriesIndexes[index + 1]?.index >= startIndex, ); if (!category) { return; } setCurrentCategory(category.key); }; const handleGoToCategory = (category: string) => { setSearching(false); const { index } = categoriesIndexes.find((item) => item.key === category) || {}; if (index === undefined) { return; } virtuosoRef.current?.scrollToIndex({ index: index > 0 ? index + 1 : 0 }); }; const handleGoToAddCustom = () => { customEmojiRoute.push(); onClose(); }; return ( } ref={emojiContainerRef}> } placeholder={t('Search')} aria-label={t('Search')} /> {emojiCategories.map((category) => ( handleGoToCategory(category.key)} /> ))} {searching && } {!searching && ( )}
{emojiToPreview && } {canManageEmoji && emojiToPreview === null && ( )}
); }; export default EmojiPicker;