Add custom emoji to emoji board (#210)
* Display custom emoji in picker Adds a single category at the start of the emoji picker to display the user's custom emoji * Show any amount of custom emoji packs in the Emoji Board * Use thumbnails in emoji picker + mark as emoji * Fix emoji picker stretching when too many packs are available * Sprinkle in a few comments for good measure * Remove emoji-less packs from the emoji picker
This commit is contained in:
parent
9ea9bf4035
commit
fd9f734de1
3 changed files with 128 additions and 27 deletions
|
@ -7,6 +7,10 @@ import './EmojiBoard.scss';
|
||||||
import parse from 'html-react-parser';
|
import parse from 'html-react-parser';
|
||||||
import twemoji from 'twemoji';
|
import twemoji from 'twemoji';
|
||||||
import { emojiGroups, emojis } from './emoji';
|
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 AsyncSearch from '../../../util/AsyncSearch';
|
||||||
|
|
||||||
import Text from '../../atoms/text/Text';
|
import Text from '../../atoms/text/Text';
|
||||||
|
@ -16,6 +20,7 @@ import Input from '../../atoms/input/Input';
|
||||||
import ScrollView from '../../atoms/scroll/ScrollView';
|
import ScrollView from '../../atoms/scroll/ScrollView';
|
||||||
|
|
||||||
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
import SearchIC from '../../../../public/res/ic/outlined/search.svg';
|
||||||
|
import StarIC from '../../../../public/res/ic/outlined/star.svg';
|
||||||
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
|
||||||
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
|
import DogIC from '../../../../public/res/ic/outlined/dog.svg';
|
||||||
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
|
import CupIC from '../../../../public/res/ic/outlined/cup.svg';
|
||||||
|
@ -40,7 +45,9 @@ function EmojiGroup({ name, groupEmojis }) {
|
||||||
emojiRow.push(
|
emojiRow.push(
|
||||||
<span key={emojiIndex}>
|
<span key={emojiIndex}>
|
||||||
{
|
{
|
||||||
parse(twemoji.parse(
|
emoji.hexcode
|
||||||
|
// This is a unicode emoji, and should be rendered with twemoji
|
||||||
|
? parse(twemoji.parse(
|
||||||
emoji.unicode,
|
emoji.unicode,
|
||||||
{
|
{
|
||||||
attributes: () => ({
|
attributes: () => ({
|
||||||
|
@ -50,6 +57,18 @@ function EmojiGroup({ name, groupEmojis }) {
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
|
// 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
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</span>,
|
</span>,
|
||||||
);
|
);
|
||||||
|
@ -72,6 +91,8 @@ EmojiGroup.propTypes = {
|
||||||
length: PropTypes.number,
|
length: PropTypes.number,
|
||||||
unicode: PropTypes.string,
|
unicode: PropTypes.string,
|
||||||
hexcode: PropTypes.string,
|
hexcode: PropTypes.string,
|
||||||
|
mxc: PropTypes.string,
|
||||||
|
shortcode: PropTypes.string,
|
||||||
shortcodes: PropTypes.oneOfType([
|
shortcodes: PropTypes.oneOfType([
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
PropTypes.arrayOf(PropTypes.string),
|
PropTypes.arrayOf(PropTypes.string),
|
||||||
|
@ -133,8 +154,8 @@ function EmojiBoard({ onSelect }) {
|
||||||
const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild;
|
const infoEmoji = emojiInfo.current.firstElementChild.firstElementChild;
|
||||||
const infoShortcode = emojiInfo.current.lastElementChild;
|
const infoShortcode = emojiInfo.current.lastElementChild;
|
||||||
|
|
||||||
const emojiSrc = infoEmoji.src;
|
infoEmoji.src = emoji.src;
|
||||||
infoEmoji.src = `${emojiSrc.slice(0, emojiSrc.lastIndexOf('/') + 1)}${emoji.hexcode.toLowerCase()}.png`;
|
infoEmoji.alt = emoji.unicode;
|
||||||
infoShortcode.textContent = `:${emoji.shortcode}:`;
|
infoShortcode.textContent = `:${emoji.shortcode}:`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,16 +163,21 @@ function EmojiBoard({ onSelect }) {
|
||||||
if (isTargetNotEmoji(e.target)) return;
|
if (isTargetNotEmoji(e.target)) return;
|
||||||
|
|
||||||
const emoji = e.target;
|
const emoji = e.target;
|
||||||
const { shortcodes, hexcode } = getEmojiDataFromTarget(emoji);
|
const { shortcodes, unicode } = getEmojiDataFromTarget(emoji);
|
||||||
|
const { src } = e.target;
|
||||||
|
|
||||||
if (typeof shortcodes === 'undefined') {
|
if (typeof shortcodes === 'undefined') {
|
||||||
searchRef.current.placeholder = 'Search';
|
searchRef.current.placeholder = 'Search';
|
||||||
setEmojiInfo({ hexcode: '1f643', shortcode: 'slight_smile' });
|
setEmojiInfo({
|
||||||
|
unicode: '🙂',
|
||||||
|
shortcode: 'slight_smile',
|
||||||
|
src: 'https://twemoji.maxcdn.com/v/13.1.0/72x72/1f642.png',
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (searchRef.current.placeholder === shortcodes[0]) return;
|
if (searchRef.current.placeholder === shortcodes[0]) return;
|
||||||
searchRef.current.setAttribute('placeholder', shortcodes[0]);
|
searchRef.current.setAttribute('placeholder', shortcodes[0]);
|
||||||
setEmojiInfo({ hexcode, shortcode: shortcodes[0] });
|
setEmojiInfo({ shortcode: shortcodes[0], src, unicode });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchChange(e) {
|
function handleSearchChange(e) {
|
||||||
|
@ -160,11 +186,44 @@ function EmojiBoard({ onSelect }) {
|
||||||
scrollEmojisRef.current.scrollTop = 0;
|
scrollEmojisRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
function openGroup(groupOrder) {
|
function openGroup(groupOrder) {
|
||||||
let tabIndex = groupOrder;
|
let tabIndex = groupOrder;
|
||||||
const $emojiContent = scrollEmojisRef.current.firstElementChild;
|
const $emojiContent = scrollEmojisRef.current.firstElementChild;
|
||||||
const groupCount = $emojiContent.childElementCount;
|
const groupCount = $emojiContent.childElementCount;
|
||||||
if (groupCount > emojiGroups.length) tabIndex += groupCount - emojiGroups.length;
|
if (groupCount > emojiGroups.length) {
|
||||||
|
tabIndex += groupCount - emojiGroups.length - availableEmojis.length;
|
||||||
|
}
|
||||||
$emojiContent.children[tabIndex].scrollIntoView();
|
$emojiContent.children[tabIndex].scrollIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,6 +238,16 @@ function EmojiBoard({ onSelect }) {
|
||||||
<ScrollView ref={scrollEmojisRef} autoHide>
|
<ScrollView ref={scrollEmojisRef} autoHide>
|
||||||
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
<div onMouseMove={hoverEmoji} onClick={selectEmoji}>
|
||||||
<SearchedEmoji />
|
<SearchedEmoji />
|
||||||
|
{
|
||||||
|
availableEmojis.map((pack) => (
|
||||||
|
<EmojiGroup
|
||||||
|
name={pack.displayName}
|
||||||
|
key={pack.packIndex}
|
||||||
|
groupEmojis={pack.getEmojis()}
|
||||||
|
className="custom-emoji-group"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
{
|
{
|
||||||
emojiGroups.map((group) => (
|
emojiGroups.map((group) => (
|
||||||
<EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
|
<EmojiGroup key={group.name} name={group.name} groupEmojis={group.emojis} />
|
||||||
|
@ -192,16 +261,42 @@ function EmojiBoard({ onSelect }) {
|
||||||
<Text>:slight_smile:</Text>
|
<Text>:slight_smile:</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollView invisible>
|
||||||
<div className="emoji-board__nav">
|
<div className="emoji-board__nav">
|
||||||
<IconButton onClick={() => openGroup(0)} src={EmojiIC} tooltip="Smileys" tooltipPlacement="right" />
|
{
|
||||||
<IconButton onClick={() => openGroup(1)} src={DogIC} tooltip="Animals" tooltipPlacement="right" />
|
availableEmojis.map((pack) => (
|
||||||
<IconButton onClick={() => openGroup(2)} src={CupIC} tooltip="Food" tooltipPlacement="right" />
|
// TODO (future PR?): Use the pack icon, and only use StarIC as a fallback
|
||||||
<IconButton onClick={() => openGroup(3)} src={BallIC} tooltip="Activity" tooltipPlacement="right" />
|
<IconButton
|
||||||
<IconButton onClick={() => openGroup(4)} src={PhotoIC} tooltip="Travel" tooltipPlacement="right" />
|
onClick={() => openGroup(pack.packIndex)}
|
||||||
<IconButton onClick={() => openGroup(5)} src={BulbIC} tooltip="Objects" tooltipPlacement="right" />
|
src={StarIC}
|
||||||
<IconButton onClick={() => openGroup(6)} src={PeaceIC} tooltip="Symbols" tooltipPlacement="right" />
|
key={pack.packIndex}
|
||||||
<IconButton onClick={() => openGroup(7)} src={FlagIC} tooltip="Flags" tooltipPlacement="right" />
|
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>
|
</div>
|
||||||
|
</ScrollView>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@
|
||||||
height: 400px;
|
height: 400px;
|
||||||
width: 286px;
|
width: 286px;
|
||||||
}
|
}
|
||||||
|
& > .scrollbar {
|
||||||
|
width: initial;
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
&__nav {
|
&__nav {
|
||||||
@extend .cp-fx__column;
|
@extend .cp-fx__column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -166,4 +166,6 @@ function getEmojiForCompletion(room) {
|
||||||
.concat(Array.from(allEmoji.values()));
|
.concat(Array.from(allEmoji.values()));
|
||||||
}
|
}
|
||||||
|
|
||||||
export { getUserImagePack, getShortcodeToEmoji, getEmojiForCompletion };
|
export {
|
||||||
|
getUserImagePack, getShortcodeToEmoji, getRelevantPacks, getEmojiForCompletion,
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue