2021-07-28 16:15:52 +03:00
|
|
|
/* 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';
|
2021-08-25 12:30:40 +03:00
|
|
|
import { emojiGroups, emojis } from './emoji';
|
2021-12-30 06:02:49 +02:00
|
|
|
import { getRelevantPacks } from './custom-emoji';
|
|
|
|
import initMatrix from '../../../client/initMatrix';
|
|
|
|
import cons from '../../../client/state/cons';
|
|
|
|
import navigation from '../../../client/state/navigation';
|
2021-08-25 12:30:40 +03:00
|
|
|
import AsyncSearch from '../../../util/AsyncSearch';
|
2021-07-28 16:15:52 +03:00
|
|
|
|
|
|
|
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';
|
2021-12-30 06:02:49 +02:00
|
|
|
import StarIC from '../../../../public/res/ic/outlined/star.svg';
|
2021-07-28 16:15:52 +03:00
|
|
|
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';
|
|
|
|
|
2021-08-25 12:30:40 +03:00
|
|
|
function EmojiGroup({ name, groupEmojis }) {
|
2021-07-28 16:15:52 +03:00
|
|
|
function getEmojiBoard() {
|
2021-08-13 14:01:22 +03:00
|
|
|
const emojiBoard = [];
|
2021-07-28 16:15:52 +03:00
|
|
|
const ROW_EMOJIS_COUNT = 7;
|
2021-08-25 12:30:40 +03:00
|
|
|
const totalEmojis = groupEmojis.length;
|
2021-07-28 16:15:52 +03:00
|
|
|
|
|
|
|
for (let r = 0; r < totalEmojis; r += ROW_EMOJIS_COUNT) {
|
|
|
|
const emojiRow = [];
|
|
|
|
for (let c = r; c < r + ROW_EMOJIS_COUNT; c += 1) {
|
2021-08-13 14:01:22 +03:00
|
|
|
const emojiIndex = c;
|
2021-07-28 16:15:52 +03:00
|
|
|
if (emojiIndex >= totalEmojis) break;
|
2021-08-25 12:30:40 +03:00
|
|
|
const emoji = groupEmojis[emojiIndex];
|
2021-07-28 16:15:52 +03:00
|
|
|
emojiRow.push(
|
|
|
|
<span key={emojiIndex}>
|
|
|
|
{
|
2021-12-30 06:02:49 +02:00
|
|
|
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,
|
|
|
|
}),
|
|
|
|
},
|
|
|
|
))
|
|
|
|
// This is a custom emoji, and should be render as an mxc
|
|
|
|
: (
|
|
|
|
<img
|
|
|
|
className="emoji"
|
|
|
|
draggable="false"
|
|
|
|
alt={emoji.shortcode}
|
|
|
|
unicode={`:${emoji.shortcode}:`}
|
|
|
|
shortcodes={emoji.shortcode}
|
|
|
|
src={initMatrix.matrixClient.mxcUrlToHttp(emoji.mxc, 38, 38, 'crop')}
|
|
|
|
data-mx-emoticon
|
|
|
|
/>
|
|
|
|
)
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
</span>,
|
|
|
|
);
|
|
|
|
}
|
2021-08-13 14:01:22 +03:00
|
|
|
emojiBoard.push(<div key={r} className="emoji-row">{emojiRow}</div>);
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
2021-08-13 14:01:22 +03:00
|
|
|
return emojiBoard;
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="emoji-group">
|
2021-12-16 14:25:16 +02:00
|
|
|
<Text className="emoji-group__header" variant="b2" weight="bold">{name}</Text>
|
2021-08-25 12:30:40 +03:00
|
|
|
{groupEmojis.length !== 0 && <div className="emoji-set">{getEmojiBoard()}</div>}
|
2021-07-28 16:15:52 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
EmojiGroup.propTypes = {
|
|
|
|
name: PropTypes.string.isRequired,
|
2021-08-25 12:30:40 +03:00
|
|
|
groupEmojis: PropTypes.arrayOf(PropTypes.shape({
|
2021-07-28 16:15:52 +03:00
|
|
|
length: PropTypes.number,
|
|
|
|
unicode: PropTypes.string,
|
2021-08-13 14:01:22 +03:00
|
|
|
hexcode: PropTypes.string,
|
2021-12-30 06:02:49 +02:00
|
|
|
mxc: PropTypes.string,
|
|
|
|
shortcode: PropTypes.string,
|
2021-07-28 16:15:52 +03:00
|
|
|
shortcodes: PropTypes.oneOfType([
|
|
|
|
PropTypes.string,
|
|
|
|
PropTypes.arrayOf(PropTypes.string),
|
|
|
|
]),
|
|
|
|
})).isRequired,
|
|
|
|
};
|
|
|
|
|
2021-08-25 12:30:40 +03:00
|
|
|
const asyncSearch = new AsyncSearch();
|
2021-10-25 14:16:23 +03:00
|
|
|
asyncSearch.setup(emojis, { keys: ['shortcode'], isContain: true, limit: 40 });
|
2021-07-28 16:15:52 +03:00
|
|
|
function SearchedEmoji() {
|
2021-08-25 12:30:40 +03:00
|
|
|
const [searchedEmojis, setSearchedEmojis] = useState(null);
|
2021-07-28 16:15:52 +03:00
|
|
|
|
2021-08-25 12:30:40 +03:00
|
|
|
function handleSearchEmoji(resultEmojis, term) {
|
|
|
|
if (term === '' || resultEmojis.length === 0) {
|
|
|
|
if (term === '') setSearchedEmojis(null);
|
2021-10-22 14:32:42 +03:00
|
|
|
else setSearchedEmojis({ emojis: [] });
|
2021-07-28 16:15:52 +03:00
|
|
|
return;
|
|
|
|
}
|
2021-10-22 14:32:42 +03:00
|
|
|
setSearchedEmojis({ emojis: resultEmojis });
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => {
|
2021-08-25 12:30:40 +03:00
|
|
|
asyncSearch.on(asyncSearch.RESULT_SENT, handleSearchEmoji);
|
2021-07-28 16:15:52 +03:00
|
|
|
return () => {
|
2021-08-25 12:30:40 +03:00
|
|
|
asyncSearch.removeListener(asyncSearch.RESULT_SENT, handleSearchEmoji);
|
2021-07-28 16:15:52 +03:00
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
2021-08-25 12:30:40 +03:00
|
|
|
if (searchedEmojis === null) return false;
|
|
|
|
|
2021-10-22 14:32:42 +03:00
|
|
|
return <EmojiGroup key="-1" name={searchedEmojis.emojis.length === 0 ? 'No search result found' : 'Search results'} groupEmojis={searchedEmojis.emojis} />;
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function EmojiBoard({ onSelect }) {
|
|
|
|
const searchRef = useRef(null);
|
|
|
|
const scrollEmojisRef = useRef(null);
|
2021-08-13 14:01:22 +03:00
|
|
|
const emojiInfo = useRef(null);
|
2021-07-28 16:15:52 +03:00
|
|
|
|
|
|
|
function isTargetNotEmoji(target) {
|
|
|
|
return target.classList.contains('emoji') === false;
|
|
|
|
}
|
|
|
|
function getEmojiDataFromTarget(target) {
|
|
|
|
const unicode = target.getAttribute('unicode');
|
2021-08-13 14:01:22 +03:00
|
|
|
const hexcode = target.getAttribute('hexcode');
|
2021-07-28 16:15:52 +03:00
|
|
|
let shortcodes = target.getAttribute('shortcodes');
|
|
|
|
if (typeof shortcodes === 'undefined') shortcodes = undefined;
|
|
|
|
else shortcodes = shortcodes.split(',');
|
2021-08-13 14:01:22 +03:00
|
|
|
return { unicode, hexcode, shortcodes };
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function selectEmoji(e) {
|
|
|
|
if (isTargetNotEmoji(e.target)) return;
|
|
|
|
|
|
|
|
const emoji = e.target;
|
|
|
|
onSelect(getEmojiDataFromTarget(emoji));
|
|
|
|
}
|
|
|
|
|
2021-08-13 14:01:22 +03:00
|
|
|
function setEmojiInfo(emoji) {
|
|
|
|
const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild;
|
|
|
|
const infoShortcode = emojiInfo.current.lastElementChild;
|
|
|
|
|
2021-12-30 06:02:49 +02:00
|
|
|
infoEmoji.src = emoji.src;
|
|
|
|
infoEmoji.alt = emoji.unicode;
|
2021-08-13 14:01:22 +03:00
|
|
|
infoShortcode.textContent = `:${emoji.shortcode}:`;
|
|
|
|
}
|
|
|
|
|
2021-07-28 16:15:52 +03:00
|
|
|
function hoverEmoji(e) {
|
|
|
|
if (isTargetNotEmoji(e.target)) return;
|
|
|
|
|
|
|
|
const emoji = e.target;
|
2021-12-30 06:02:49 +02:00
|
|
|
const { shortcodes, unicode } = getEmojiDataFromTarget(emoji);
|
|
|
|
const { src } = e.target;
|
2021-07-28 16:15:52 +03:00
|
|
|
|
|
|
|
if (typeof shortcodes === 'undefined') {
|
|
|
|
searchRef.current.placeholder = 'Search';
|
2021-12-30 06:02:49 +02:00
|
|
|
setEmojiInfo({
|
|
|
|
unicode: '🙂',
|
|
|
|
shortcode: 'slight_smile',
|
|
|
|
src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png',
|
|
|
|
});
|
2021-07-28 16:15:52 +03:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (searchRef.current.placeholder === shortcodes[0]) return;
|
2021-08-25 12:30:40 +03:00
|
|
|
searchRef.current.setAttribute('placeholder', shortcodes[0]);
|
2021-12-30 06:02:49 +02:00
|
|
|
setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleSearchChange(e) {
|
|
|
|
const term = e.target.value;
|
2021-08-25 12:30:40 +03:00
|
|
|
asyncSearch.search(term);
|
|
|
|
scrollEmojisRef.current.scrollTop = 0;
|
2021-07-28 16:15:52 +03:00
|
|
|
}
|
|
|
|
|
2021-12-30 06:02:49 +02:00
|
|
|
const [availableEmojis, setAvailableEmojis] = useState([]);
|
|
|
|
|
|
|
|
// This should be called whenever the room changes, so that we can switch out the emoji
|
|
|
|
// for whatever packs are relevant to this room
|
|
|
|
function updateAvailableEmoji(selectedRoomId) {
|
|
|
|
// Retrieve the packs for the new room
|
|
|
|
const packs = getRelevantPacks(
|
|
|
|
initMatrix.matrixClient.getRoom(selectedRoomId),
|
|
|
|
)
|
|
|
|
// Remove packs that aren't marked as emoji packs
|
|
|
|
.filter((pack) => pack.usage.indexOf('emoticon') !== -1)
|
|
|
|
// Remove packs without emojis
|
|
|
|
.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;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the component state
|
|
|
|
setAvailableEmojis(packs);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register the above function as an event listener
|
|
|
|
useEffect(() => {
|
|
|
|
navigation.on(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
|
|
|
return () => {
|
|
|
|
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, updateAvailableEmoji);
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
2021-07-28 16:15:52 +03:00
|
|
|
function openGroup(groupOrder) {
|
|
|
|
let tabIndex = groupOrder;
|
|
|
|
const $emojiContent = scrollEmojisRef.current.firstElementChild;
|
|
|
|
const groupCount = $emojiContent.childElementCount;
|
2021-12-30 06:02:49 +02:00
|
|
|
if (groupCount > emojiGroups.length) {
|
|
|
|
tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
|
|
|
|
}
|
2021-07-28 16:15:52 +03:00
|
|
|
$emojiContent.children[tabIndex].scrollIntoView();
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div id="emoji-board" className="emoji-board">
|
|
|
|
<div className="emoji-board__content">
|
2021-08-13 14:01:22 +03:00
|
|
|
<div className="emoji-board__content__search">
|
|
|
|
<RawIcon size="small" src={SearchIC} />
|
|
|
|
<Input onChange={handleSearchChange} forwardRef={searchRef} placeholder="Search" />
|
|
|
|
</div>
|
|
|
|
<div className="emoji-board__content__emojis">
|
2021-07-28 16:15:52 +03:00
|
|
|
<ScrollView ref={scrollEmojisRef} autoHide>
|
|
|
|
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
|
|
|
<SearchedEmoji />
|
2021-12-30 06:02:49 +02:00
|
|
|
{
|
|
|
|
availableEmojis.map((pack) => (
|
|
|
|
<EmojiGroup
|
|
|
|
name={pack.displayName}
|
|
|
|
key={pack.packIndex}
|
|
|
|
groupEmojis={pack.getEmojis()}
|
|
|
|
className="custom-emoji-group"
|
|
|
|
/>
|
|
|
|
))
|
|
|
|
}
|
2021-07-28 16:15:52 +03:00
|
|
|
{
|
|
|
|
emojiGroups.map((group) => (
|
2021-08-25 12:30:40 +03:00
|
|
|
<EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
|
2021-07-28 16:15:52 +03:00
|
|
|
))
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
</ScrollView>
|
|
|
|
</div>
|
2021-08-13 14:01:22 +03:00
|
|
|
<div ref={emojiInfo} className="emoji-board__content__info">
|
|
|
|
<div>{ parse(twemoji.parse('🙂')) }</div>
|
|
|
|
<Text>:slight_smile:</Text>
|
2021-07-28 16:15:52 +03:00
|
|
|
</div>
|
|
|
|
</div>
|
2021-12-30 06:02:49 +02:00
|
|
|
<ScrollView invisible>
|
|
|
|
<div className="emoji-board__nav">
|
|
|
|
{
|
|
|
|
availableEmojis.map((pack) => (
|
|
|
|
// TODO (future PR?): Use the pack icon, and only use StarIC as a fallback
|
|
|
|
<IconButton
|
|
|
|
onClick={() => openGroup(pack.packIndex)}
|
|
|
|
src={StarIC}
|
|
|
|
key={pack.packIndex}
|
|
|
|
tooltip={pack.displayName}
|
|
|
|
tooltipPlacement="right"
|
|
|
|
/>
|
|
|
|
))
|
|
|
|
}
|
|
|
|
{
|
|
|
|
[
|
|
|
|
[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]) => (
|
|
|
|
<IconButton
|
|
|
|
onClick={() => openGroup(availableEmojis.length + indx)}
|
|
|
|
key={indx}
|
|
|
|
src={ico}
|
|
|
|
tooltip={name}
|
|
|
|
tooltipPlacement="right"
|
|
|
|
/>
|
|
|
|
))
|
|
|
|
}
|
|
|
|
</div>
|
|
|
|
</ScrollView>
|
2021-07-28 16:15:52 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
EmojiBoard.propTypes = {
|
|
|
|
onSelect: PropTypes.func.isRequired,
|
|
|
|
};
|
|
|
|
|
|
|
|
export default EmojiBoard;
|