Zip shortcodes in with emoji objects
Signed-off-by: Robin Townsend <robin@robin.town>pull/21833/head
parent
8efb30eb07
commit
f88d5dd24e
|
@ -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]}:` : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
),
|
),
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
38
src/emoji.ts
38
src/emoji.ts
|
@ -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;
|
|
||||||
|
|
Loading…
Reference in New Issue