/* eslint-disable jsx-a11y/no-static-element-interactions */ /* eslint-disable jsx-a11y/click-events-have-key-events */ import React, { useState, useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import './EmojiBoard.scss'; import parse from 'html-react-parser'; import twemoji from 'twemoji'; import { emojiGroups, emojis } from './emoji'; import { getRelevantPacks } from './custom-emoji'; import initMatrix from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import AsyncSearch from '../../../util/AsyncSearch'; import { addRecentEmoji, getRecentEmojis } from './recent'; import Text from '../../atoms/text/Text'; import RawIcon from '../../atoms/system-icons/RawIcon'; import IconButton from '../../atoms/button/IconButton'; import Input from '../../atoms/input/Input'; import ScrollView from '../../atoms/scroll/ScrollView'; import SearchIC from '../../../../public/res/ic/outlined/search.svg'; import RecentClockIC from '../../../../public/res/ic/outlined/recent-clock.svg'; import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg'; import DogIC from '../../../../public/res/ic/outlined/dog.svg'; import CupIC from '../../../../public/res/ic/outlined/cup.svg'; import BallIC from '../../../../public/res/ic/outlined/ball.svg'; import PhotoIC from '../../../../public/res/ic/outlined/photo.svg'; import BulbIC from '../../../../public/res/ic/outlined/bulb.svg'; import PeaceIC from '../../../../public/res/ic/outlined/peace.svg'; import FlagIC from '../../../../public/res/ic/outlined/flag.svg'; const ROW_EMOJIS_COUNT = 7; const EmojiGroup = React.memo(({ name, groupEmojis }) => { function getEmojiBoard() { const emojiBoard = []; const totalEmojis = groupEmojis.length; for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) { const emojiRow = []; for (let c = r; c < r + ROW_EMOJIS_COUNT; c += 1) { const emojiIndex = c; if (emojiIndex >= totalEmojis) break; const emoji = groupEmojis[emojiIndex]; emojiRow.push( { emoji.hexcode // This is a unicode emoji, and should be rendered with twemoji ? parse(twemoji.parse( emoji.unicode, { attributes: () => ({ unicode: emoji.unicode, shortcodes: emoji.shortcodes?.toString(), hexcode: emoji.hexcode, loading: 'lazy', }), }, )) // This is a custom emoji, and should be render as an mxc : ( {emoji.shortcode} ) } , ); } emojiBoard.push(
{emojiRow}
); } return emojiBoard; } return (
{name} {groupEmojis.length !== 0 &&
{getEmojiBoard()}
}
); }); EmojiGroup.propTypes = { name: PropTypes.string.isRequired, groupEmojis: PropTypes.arrayOf(PropTypes.shape({ length: PropTypes.number, unicode: PropTypes.string, hexcode: PropTypes.string, mxc: PropTypes.string, shortcode: PropTypes.string, shortcodes: PropTypes.oneOfType([ PropTypes.string, PropTypes.arrayOf(PropTypes.string), ]), })).isRequired, }; const asyncSearch = new AsyncSearch(); asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 40 }); function SearchedEmoji() { const [searchedEmojis, setSearchedEmojis] = useState(null); function handleSearchEmoji(resultEmojis, term) { if (term === '' || resultEmojis.length === 0) { if (term === '') setSearchedEmojis(null); else setSearchedEmojis({ emojis: [] }); return; } setSearchedEmojis({ emojis: resultEmojis }); } useEffect(() => { asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchEmoji); return () => { asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchEmoji); }; }, []); if (searchedEmojis === null) return false; return ; } function EmojiBoard({ onSelect, searchRef }) { const scrollEmojisRef = useRef(null); const emojiInfo = useRef(null); function isTargetNotEmoji(target) { return target.classList.contains('emoji') === false; } function getEmojiDataFromTarget(target) { const unicode = target.getAttribute('unicode'); const hexcode = target.getAttribute('hexcode'); let shortcodes = target.getAttribute('shortcodes'); if (typeof shortcodes === 'undefined') shortcodes = undefined; else shortcodes = shortcodes.split(','); return { unicode, hexcode, shortcodes }; } function selectEmoji(e) { if (isTargetNotEmoji(e.target)) return; const emoji = getEmojiDataFromTarget(e.target); onSelect(emoji); if (emoji.hexcode) addRecentEmoji(emoji.unicode); } function setEmojiInfo(emoji) { const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild; const infoShortcode = emojiInfo.current.lastElementChild; infoEmoji.src = emoji.src; infoEmoji.alt = emoji.unicode; infoShortcode.textContent = `:${emoji.shortcode}:`; } function hoverEmoji(e) { if (isTargetNotEmoji(e.target)) return; const emoji = e.target; const { shortcodes, unicode } = getEmojiDataFromTarget(emoji); const { src } = e.target; if (typeof shortcodes === 'undefined') { searchRef.current.placeholder = 'Search'; setEmojiInfo({ unicode: '🙂', shortcode: 'slight_smile', src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png', }); return; } if (searchRef.current.placeholder === shortcodes[0]) return; searchRef.current.setAttribute('placeholder', shortcodes[0]); setEmojiInfo({ shortcode: shortcodes[0], src, unicode }); } function handleSearchChange() { const term = searchRef.current.value; asyncSearch.search(term); scrollEmojisRef.current.scrollTop = 0; } const [availableEmojis, setAvailableEmojis] = useState([]); const [recentEmojis, setRecentEmojis] = useState([]); const recentOffset = recentEmojis.length > 0 ? 1 : 0; useEffect(() => { const updateAvailableEmoji = (selectedRoomId) => { if (!selectedRoomId) { setAvailableEmojis([]); return; } // Retrieve the packs for the new room // Remove packs that aren't marked as emoji packs // Remove packs without emojis const packs = getRelevantPacks( initMatrix.matrixClient.getRoom(selectedRoomId), ) .filter((pack) => pack.usage.indexOf('emoticon') !== -1) .filter((pack) => pack.getEmojis().length !== 0); // Set an index for each pack so that we know where to jump when the user uses the nav for (let i = 0; i < packs.length; i += 1) { packs[i].packIndex = i; } setAvailableEmojis(packs); }; const onOpen = () => { searchRef.current.value = ''; handleSearchChange(); // only update when board is getting opened to prevent shifting UI setRecentEmojis(getRecentEmojis(3 * ROW_EMOJIS_COUNT)); }; navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji); navigation.on(cons.events.navigation.EMOJIBOARD_OPENED, onOpen); return () => { navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji); navigation.removeListener(cons.events.navigation.EMOJIBOARD_OPENED, onOpen); }; }, []); function openGroup(groupOrder) { let tabIndex = groupOrder; const $emojiContent = scrollEmojisRef.current.firstElementChild; const groupCount = $emojiContent.childElementCount; if (groupCount > emojiGroups.length) { tabIndex += groupCount - emojiGroups.length - availableEmojis.length - recentOffset; } $emojiContent.children[tabIndex].scrollIntoView(); } return (
{recentEmojis.length > 0 && } { availableEmojis.map((pack) => ( )) } { emojiGroups.map((group) => ( )) }
{ parse(twemoji.parse('🙂')) }
:slight_smile:
{recentEmojis.length > 0 && ( openGroup(0)} src={RecentClockIC} tooltip="Recent" tooltipPlacement="right" /> )}
{ availableEmojis.map((pack) => { const src = initMatrix.matrixClient.mxcUrlToHttp(pack.avatar ?? pack.images[0].mxc); return ( openGroup(recentOffset + pack.packIndex)} src={src} key={pack.packIndex} tooltip={pack.displayName} tooltipPlacement="right" isImage /> ); }) }
{ [ [0, EmojiIC, 'Smilies'], [1, DogIC, 'Animals'], [2, CupIC, 'Food'], [3, BallIC, 'Activities'], [4, PhotoIC, 'Travel'], [5, BulbIC, 'Objects'], [6, PeaceIC, 'Symbols'], [7, FlagIC, 'Flags'], ].map(([indx, ico, name]) => ( openGroup(recentOffset + availableEmojis.length + indx)} key={indx} src={ico} tooltip={name} tooltipPlacement="right" /> )) }
); } EmojiBoard.propTypes = { onSelect: PropTypes.func.isRequired, searchRef: PropTypes.shape({}).isRequired, }; export default EmojiBoard;