Zip shortcodes in with emoji objects

Signed-off-by: Robin Townsend <robin@robin.town>
pull/21833/head
Robin Townsend 2021-07-16 16:36:03 -04:00
parent 8efb30eb07
commit f88d5dd24e
5 changed files with 37 additions and 50 deletions

View File

@ -34,7 +34,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import linkifyMatrix from './linkify-matrix'; import linkifyMatrix from './linkify-matrix';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks"; import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { getEmojiFromUnicode, getShortcodes } from "./emoji"; import { getEmojiFromUnicode } from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread"; import ReplyThread from "./components/views/elements/ReplyThread";
import { mediaFromMxc } from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
@ -80,7 +80,7 @@ function mightContainEmoji(str: string): boolean {
* @return {String} The shortcode (such as :thumbup:) * @return {String} The shortcode (such as :thumbup:)
*/ */
export function unicodeToShortcode(char: string): string { export function unicodeToShortcode(char: string): string {
const shortcodes = getShortcodes(getEmojiFromUnicode(char)); const shortcodes = getEmojiFromUnicode(char).shortcodes;
return shortcodes.length > 0 ? `:${shortcodes[0]}:` : ''; return shortcodes.length > 0 ? `:${shortcodes[0]}:` : '';
} }

View File

@ -25,7 +25,7 @@ import { PillCompletion } from './Components';
import { ICompletion, ISelectionRange } from './Autocompleter'; import { ICompletion, ISelectionRange } from './Autocompleter';
import { uniq, sortBy } from 'lodash'; import { uniq, sortBy } from 'lodash';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import { EMOJI, IEmoji, getShortcodes } from '../emoji'; import { EMOJI, IEmoji } from '../emoji';
import EMOTICON_REGEX from 'emojibase-regex/emoticon'; import EMOTICON_REGEX from 'emojibase-regex/emoticon';
@ -37,8 +37,6 @@ const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]
interface IEmojiShort { interface IEmojiShort {
emoji: IEmoji; emoji: IEmoji;
shortcode: string;
altShortcodes: string[];
_orderBy: number; _orderBy: number;
} }
@ -47,16 +45,11 @@ const EMOJI_SHORTCODES: IEmojiShort[] = EMOJI.sort((a, b) => {
return a.order - b.order; return a.order - b.order;
} }
return a.group - b.group; return a.group - b.group;
}).map((emoji, index) => { }).map((emoji, index) => ({
const [shortcode, ...altShortcodes] = getShortcodes(emoji); emoji,
return { // Include the index so that we can preserve the original order
emoji, _orderBy: index,
shortcode: shortcode ? `:${shortcode}:` : undefined, })).filter(o => o.emoji.shortcodes[0]);
altShortcodes: altShortcodes.map(s => `:${s}:`),
// Include the index so that we can preserve the original order
_orderBy: index,
};
}).filter(emoji => emoji.shortcode);
function score(query, space) { function score(query, space) {
const index = space.indexOf(query); const index = space.indexOf(query);
@ -74,10 +67,8 @@ export default class EmojiProvider extends AutocompleteProvider {
constructor() { constructor() {
super(EMOJI_REGEX); super(EMOJI_REGEX);
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTCODES, { this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTCODES, {
keys: ['emoji.emoticon', 'shortcode'], keys: ['emoji.emoticon'],
funcs: [ funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`).join(" ")],
o => o.altShortcodes.join(" "), // aliases
],
// For matching against ascii equivalents // For matching against ascii equivalents
shouldMatchWordsOnly: false, shouldMatchWordsOnly: false,
}); });
@ -112,16 +103,16 @@ export default class EmojiProvider extends AutocompleteProvider {
sorters.push(c => score(matchedString, c.emoji.emoticon || "")); sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
// then sort by score (Infinity if matchedString not in shortcode) // then sort by score (Infinity if matchedString not in shortcode)
sorters.push(c => score(matchedString, c.shortcode)); sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
// then sort by max score of all shortcodes, trim off the `:` // then sort by max score of all shortcodes, trim off the `:`
sorters.push(c => Math.min( sorters.push(c => Math.min(
...[c.shortcode, ...c.altShortcodes].map(s => score(matchedString.substring(1), s)), ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
)); ));
// If the matchedString is not empty, sort by length of shortcode. Example: // If the matchedString is not empty, sort by length of shortcode. Example:
// matchedString = ":bookmark" // matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...] // completions = [":bookmark:", ":bookmark_tabs:", ...]
if (matchedString.length > 1) { if (matchedString.length > 1) {
sorters.push(c => c.shortcode.length); sorters.push(c => c.emoji.shortcodes[0].length);
} }
// Finally, sort by original ordering // Finally, sort by original ordering
sorters.push(c => c._orderBy); sorters.push(c => c._orderBy);
@ -130,7 +121,7 @@ export default class EmojiProvider extends AutocompleteProvider {
completions = completions.map(c => ({ completions = completions.map(c => ({
completion: c.emoji.unicode, completion: c.emoji.unicode,
component: ( component: (
<PillCompletion title={c.shortcode} aria-label={c.emoji.unicode}> <PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
<span>{ c.emoji.unicode }</span> <span>{ c.emoji.unicode }</span>
</PillCompletion> </PillCompletion>
), ),

View File

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { IEmoji, getShortcodes } from "../../../emoji"; import { IEmoji } from "../../../emoji";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
@ -27,11 +27,7 @@ interface IProps {
@replaceableComponent("views.emojipicker.Preview") @replaceableComponent("views.emojipicker.Preview")
class Preview extends React.PureComponent<IProps> { class Preview extends React.PureComponent<IProps> {
render() { render() {
const { const { unicode, annotation, shortcodes: [shortcode] } = this.props.emoji;
unicode = "",
annotation = "",
} = this.props.emoji;
const shortcode = getShortcodes(this.props.emoji)[0];
return ( return (
<div className="mx_EmojiPicker_footer mx_EmojiPicker_preview"> <div className="mx_EmojiPicker_footer mx_EmojiPicker_preview">

View File

@ -18,7 +18,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { getEmojiFromUnicode, getShortcodes, IEmoji } from "../../../emoji"; import { getEmojiFromUnicode, IEmoji } from "../../../emoji";
import Emoji from "./Emoji"; import Emoji from "./Emoji";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -62,7 +62,7 @@ class QuickReactions extends React.Component<IProps, IState> {
}; };
render() { render() {
const shortcode = this.state.hover ? getShortcodes(this.state.hover)[0] : undefined; const shortcode = this.state.hover?.shortcodes?.[0];
return ( return (
<section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category"> <section className="mx_EmojiPicker_footer mx_EmojiPicker_quick mx_EmojiPicker_category">
<h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label"> <h2 className="mx_EmojiPicker_quick_header mx_EmojiPicker_category_label">

View File

@ -22,26 +22,19 @@ export interface IEmoji {
group?: number; group?: number;
hexcode: string; hexcode: string;
order?: number; order?: number;
shortcodes: string[];
tags?: string[]; tags?: string[];
unicode: string; unicode: string;
emoticon?: string; emoticon?: string;
} filterString: string;
interface IEmojiWithFilterString extends IEmoji {
filterString?: string;
} }
// The unicode is stored without the variant selector // The unicode is stored without the variant selector
const UNICODE_TO_EMOJI = new Map<string, IEmojiWithFilterString>(); // not exported as gets for it are handled by getEmojiFromUnicode const UNICODE_TO_EMOJI = new Map<string, IEmoji>(); // not exported as gets for it are handled by getEmojiFromUnicode
export const EMOTICON_TO_EMOJI = new Map<string, IEmojiWithFilterString>(); export const EMOTICON_TO_EMOJI = new Map<string, IEmoji>();
export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode)); export const getEmojiFromUnicode = unicode => UNICODE_TO_EMOJI.get(stripVariation(unicode));
const toArray = (shortcodes?: string | string[]): string[] =>
typeof shortcodes === "string" ? [shortcodes] : (shortcodes ?? []);
export const getShortcodes = (emoji: IEmoji): string[] =>
toArray(SHORTCODES[emoji.hexcode]);
const EMOJIBASE_GROUP_ID_TO_CATEGORY = [ const EMOJIBASE_GROUP_ID_TO_CATEGORY = [
"people", // smileys "people", // smileys
"people", // actually people "people", // actually people
@ -69,17 +62,24 @@ export const DATA_BY_CATEGORY = {
const ZERO_WIDTH_JOINER = "\u200D"; const ZERO_WIDTH_JOINER = "\u200D";
// Store various mappings from unicode/emoticon/shortcode to the Emoji objects // Store various mappings from unicode/emoticon/shortcode to the Emoji objects
EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => { export const EMOJI: IEmoji[] = EMOJIBASE.map(emojiData => {
const shortcodes = getShortcodes(emoji); const shortcodeData = SHORTCODES[emojiData.hexcode];
// Homogenize shortcodes by ensuring that everything is an array
const shortcodes = typeof shortcodeData === "string" ? [shortcodeData] : (shortcodeData ?? []);
const emoji: IEmoji = {
...emojiData,
shortcodes,
// This is used as the string to match the query against when filtering emojis
filterString: (`${emojiData.annotation}\n${shortcodes.join('\n')}}\n${emojiData.emoticon || ''}\n` +
`${emojiData.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase(),
};
const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group]; const categoryId = EMOJIBASE_GROUP_ID_TO_CATEGORY[emoji.group];
if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) { if (DATA_BY_CATEGORY.hasOwnProperty(categoryId)) {
DATA_BY_CATEGORY[categoryId].push(emoji); DATA_BY_CATEGORY[categoryId].push(emoji);
} }
// This is used as the string to match the query against when filtering emojis
emoji.filterString = (`${emoji.annotation}\n${shortcodes.join('\n')}}\n${emoji.emoticon || ''}\n` +
`${emoji.unicode.split(ZERO_WIDTH_JOINER).join("\n")}`).toLowerCase();
// Add mapping from unicode to Emoji object // Add mapping from unicode to Emoji object
// The 'unicode' field that we use in emojibase has either // The 'unicode' field that we use in emojibase has either
// VS15 or VS16 appended to any characters that can take // VS15 or VS16 appended to any characters that can take
@ -93,6 +93,8 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
// Add mapping from emoticon to Emoji object // Add mapping from emoticon to Emoji object
EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji); EMOTICON_TO_EMOJI.set(emoji.emoticon, emoji);
} }
return emoji;
}); });
/** /**
@ -106,5 +108,3 @@ EMOJIBASE.forEach((emoji: IEmojiWithFilterString) => {
function stripVariation(str) { function stripVariation(str) {
return str.replace(/[\uFE00-\uFE0F]$/, ""); return str.replace(/[\uFE00-\uFE0F]$/, "");
} }
export const EMOJI: IEmoji[] = EMOJIBASE;