Fix a number of emoji bugs in message composer
This commit is contained in:
parent
fd5af8bb62
commit
8659f1dd23
6 changed files with 237 additions and 87 deletions
|
@ -1,11 +1,9 @@
|
||||||
const { take } = require('lodash');
|
const { take } = require('lodash');
|
||||||
const { getRecentEmojis } = require('./data');
|
const { getRecentEmojis } = require('./data');
|
||||||
const { replaceColons } = require('../../ts/components/emoji/lib');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getInitialState,
|
getInitialState,
|
||||||
load,
|
load,
|
||||||
replaceColons,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let initialState = null;
|
let initialState = null;
|
||||||
|
|
|
@ -5875,6 +5875,7 @@ $timer-icons: '55', '50', '45', '40', '35', '30', '25', '20', '15', '10', '05',
|
||||||
.module-emoji {
|
.module-emoji {
|
||||||
display: block;
|
display: block;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
|
font-family: auto;
|
||||||
|
|
||||||
@include light-theme() {
|
@include light-theme() {
|
||||||
caret-color: $color-gray-90;
|
caret-color: $color-gray-90;
|
||||||
|
|
|
@ -23,8 +23,9 @@ import { Emoji } from './emoji/Emoji';
|
||||||
import { EmojiPickDataType } from './emoji/EmojiPicker';
|
import { EmojiPickDataType } from './emoji/EmojiPicker';
|
||||||
import {
|
import {
|
||||||
convertShortName,
|
convertShortName,
|
||||||
|
DataFromEmojiText,
|
||||||
EmojiData,
|
EmojiData,
|
||||||
replaceColons,
|
emojiToData,
|
||||||
search,
|
search,
|
||||||
} from './emoji/lib';
|
} from './emoji/lib';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
@ -56,9 +57,11 @@ export type InputApi = {
|
||||||
submit: () => void;
|
submit: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SelectionKeys = 'Shift-End' | 'End' | 'Shift-Home' | 'Home';
|
||||||
export type CompositionInputEditorCommand =
|
export type CompositionInputEditorCommand =
|
||||||
| DraftEditorCommand
|
| DraftEditorCommand
|
||||||
| ('enter-emoji' | 'next-emoji' | 'prev-emoji' | 'submit');
|
| ('enter-emoji' | 'next-emoji' | 'prev-emoji' | 'submit')
|
||||||
|
| SelectionKeys;
|
||||||
|
|
||||||
function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
|
function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
|
||||||
let match;
|
let match;
|
||||||
|
@ -117,20 +120,119 @@ function getLengthOfSelectedText(state: EditorState): number {
|
||||||
return length;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWordAtIndex(str: string, index: number) {
|
function getWordAtIndex(
|
||||||
|
str: string,
|
||||||
|
index: number
|
||||||
|
): { start: number; end: number; word: string } {
|
||||||
const start = str
|
const start = str
|
||||||
.slice(0, index + 1)
|
.slice(0, index + 1)
|
||||||
.replace(/\s+$/, '')
|
.replace(/\s+$/, '')
|
||||||
.search(/\S+$/);
|
.search(/\S+$/);
|
||||||
const end = str.slice(index).search(/(?:[^a-z0-9-_+]|$)/) + index;
|
|
||||||
|
let end =
|
||||||
|
str
|
||||||
|
.slice(index)
|
||||||
|
.split('')
|
||||||
|
.findIndex(c => /[^a-z0-9-_]/i.test(c) || c === ':') + index;
|
||||||
|
|
||||||
|
const endChar = str[end];
|
||||||
|
|
||||||
|
if (/\w|:/.test(endChar)) {
|
||||||
|
end += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const word = str.slice(start, end);
|
||||||
|
|
||||||
|
if (word === ':') {
|
||||||
|
return getWordAtIndex(str, index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
word: str.slice(start, end),
|
word,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace bare (non-entitied) emojis with draft entities
|
||||||
|
function replaceBareEmojis(state: EditorState, focus: boolean): EditorState {
|
||||||
|
// Track emoji positions
|
||||||
|
const selections: Array<[SelectionState, DataFromEmojiText]> = [];
|
||||||
|
|
||||||
|
const content = state.getCurrentContent();
|
||||||
|
const initialSelection = state.getSelection();
|
||||||
|
let selectionOffset = 0;
|
||||||
|
|
||||||
|
content.getBlockMap().forEach(block => {
|
||||||
|
if (!block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pat = emojiRegex();
|
||||||
|
const text = block.getText();
|
||||||
|
let match;
|
||||||
|
// tslint:disable-next-line
|
||||||
|
while ((match = pat.exec(text)) !== null) {
|
||||||
|
const start = match.index;
|
||||||
|
const end = start + match[0].length;
|
||||||
|
const blockKey = block.getKey();
|
||||||
|
const blockSelection = SelectionState.createEmpty(blockKey).merge({
|
||||||
|
anchorOffset: start,
|
||||||
|
focusOffset: end,
|
||||||
|
}) as SelectionState;
|
||||||
|
const emojiData = emojiToData(match[0]);
|
||||||
|
// If there is not entity at this location and emoji data exists for the emoji at this location, track it for replacement
|
||||||
|
if (!block.getEntityAt(start) && emojiData) {
|
||||||
|
selections.push([blockSelection, emojiData]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newContent = selections.reduce(
|
||||||
|
(accContent, [sel, { shortName, tone }]) => {
|
||||||
|
const emojiContent = convertShortName(shortName);
|
||||||
|
const emojiEntityKey = accContent
|
||||||
|
.createEntity('emoji', 'IMMUTABLE', {
|
||||||
|
shortName: shortName,
|
||||||
|
skinTone: tone,
|
||||||
|
})
|
||||||
|
.getLastCreatedEntityKey();
|
||||||
|
|
||||||
|
// Keep track of selection offsets caused by replaced emojis
|
||||||
|
if (sel.getAnchorOffset() < initialSelection.getAnchorOffset()) {
|
||||||
|
selectionOffset += Math.abs(
|
||||||
|
sel.getAnchorOffset() - sel.getFocusOffset()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Modifier.replaceText(
|
||||||
|
accContent,
|
||||||
|
sel,
|
||||||
|
emojiContent,
|
||||||
|
undefined,
|
||||||
|
emojiEntityKey
|
||||||
|
);
|
||||||
|
},
|
||||||
|
content
|
||||||
|
);
|
||||||
|
|
||||||
|
const pushState = EditorState.push(
|
||||||
|
state,
|
||||||
|
newContent,
|
||||||
|
'replace-emoji' as EditorChangeType
|
||||||
|
);
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
const newSelection = initialSelection.merge({
|
||||||
|
anchorOffset: initialSelection.getAnchorOffset() + selectionOffset,
|
||||||
|
focusOffset: initialSelection.getFocusOffset() + selectionOffset,
|
||||||
|
}) as SelectionState;
|
||||||
|
|
||||||
|
return EditorState.forceSelection(pushState, newSelection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pushState;
|
||||||
|
}
|
||||||
|
|
||||||
const compositeDecorator = new CompositeDecorator([
|
const compositeDecorator = new CompositeDecorator([
|
||||||
{
|
{
|
||||||
strategy: (block, cb) => {
|
strategy: (block, cb) => {
|
||||||
|
@ -187,18 +289,15 @@ const getInitialEditorState = (startingText?: string) => {
|
||||||
return EditorState.createEmpty(compositeDecorator);
|
return EditorState.createEmpty(compositeDecorator);
|
||||||
}
|
}
|
||||||
|
|
||||||
const end = startingText.length;
|
const state = replaceBareEmojis(
|
||||||
const state = EditorState.createWithContent(
|
EditorState.createWithContent(
|
||||||
ContentState.createFromText(startingText),
|
ContentState.createFromText(startingText),
|
||||||
compositeDecorator
|
compositeDecorator
|
||||||
|
),
|
||||||
|
false
|
||||||
);
|
);
|
||||||
const selection = state.getSelection();
|
|
||||||
const selectionAtEnd = selection.merge({
|
|
||||||
anchorOffset: end,
|
|
||||||
focusOffset: end,
|
|
||||||
}) as SelectionState;
|
|
||||||
|
|
||||||
return EditorState.forceSelection(state, selectionAtEnd);
|
return EditorState.moveFocusToEnd(state);
|
||||||
};
|
};
|
||||||
|
|
||||||
// tslint:disable-next-line max-func-body-length
|
// tslint:disable-next-line max-func-body-length
|
||||||
|
@ -231,6 +330,7 @@ export const CompositionInput = ({
|
||||||
const focusRef = React.useRef(false);
|
const focusRef = React.useRef(false);
|
||||||
const editorStateRef = React.useRef<EditorState>(editorRenderState);
|
const editorStateRef = React.useRef<EditorState>(editorRenderState);
|
||||||
const rootElRef = React.useRef<HTMLDivElement>();
|
const rootElRef = React.useRef<HTMLDivElement>();
|
||||||
|
const latestKeyRef = React.useRef<string>();
|
||||||
|
|
||||||
// This function sets editorState and also keeps a reference to the newly set
|
// This function sets editorState and also keeps a reference to the newly set
|
||||||
// state so we can reference the state in effects and callbacks without
|
// state so we can reference the state in effects and callbacks without
|
||||||
|
@ -309,7 +409,7 @@ export const CompositionInput = ({
|
||||||
|
|
||||||
// Update the state to indicate emojiable text at the current position.
|
// Update the state to indicate emojiable text at the current position.
|
||||||
const newSearchText = match ? match.trim().substr(1) : '';
|
const newSearchText = match ? match.trim().substr(1) : '';
|
||||||
if (newSearchText.endsWith(':')) {
|
if (newSearchText.endsWith(':') && latestKeyRef.current === ':') {
|
||||||
const bareText = trimEnd(newSearchText, ':');
|
const bareText = trimEnd(newSearchText, ':');
|
||||||
const emoji = head(search(bareText));
|
const emoji = head(search(bareText));
|
||||||
if (emoji && bareText === emoji.short_name) {
|
if (emoji && bareText === emoji.short_name) {
|
||||||
|
@ -328,12 +428,15 @@ export const CompositionInput = ({
|
||||||
resetEmojiResults();
|
resetEmojiResults();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const modifiedState = replaceBareEmojis(newState, focusRef.current);
|
||||||
|
|
||||||
// Finally, update the editor state
|
// Finally, update the editor state
|
||||||
setAndTrackEditorState(newState);
|
setAndTrackEditorState(modifiedState);
|
||||||
updateExternalStateListeners(newState);
|
updateExternalStateListeners(modifiedState);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
focusRef,
|
focusRef,
|
||||||
|
latestKeyRef,
|
||||||
resetEmojiResults,
|
resetEmojiResults,
|
||||||
setAndTrackEditorState,
|
setAndTrackEditorState,
|
||||||
setSearchText,
|
setSearchText,
|
||||||
|
@ -399,8 +502,7 @@ export const CompositionInput = ({
|
||||||
() => {
|
() => {
|
||||||
const { current: state } = editorStateRef;
|
const { current: state } = editorStateRef;
|
||||||
const text = state.getCurrentContent().getPlainText();
|
const text = state.getCurrentContent().getPlainText();
|
||||||
const emojidText = replaceColons(text);
|
const trimmedText = text.trim();
|
||||||
const trimmedText = emojidText.trim();
|
|
||||||
onSubmit(trimmedText);
|
onSubmit(trimmedText);
|
||||||
},
|
},
|
||||||
[editorStateRef, onSubmit]
|
[editorStateRef, onSubmit]
|
||||||
|
@ -453,8 +555,36 @@ export const CompositionInput = ({
|
||||||
[emojiResultsIndex, emojiResults]
|
[emojiResultsIndex, emojiResults]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const setCursor = React.useCallback(
|
||||||
|
(key: SelectionKeys) => {
|
||||||
|
const { current: state } = editorStateRef;
|
||||||
|
const selection = state.getSelection();
|
||||||
|
const offset =
|
||||||
|
key === 'Shift-Home' || key === 'Home'
|
||||||
|
? 0
|
||||||
|
: state
|
||||||
|
.getCurrentContent()
|
||||||
|
.getBlockForKey(selection.getAnchorKey())
|
||||||
|
.getText().length;
|
||||||
|
|
||||||
|
const desc: { focusOffset?: number; anchorOffset?: number } = {
|
||||||
|
focusOffset: offset,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (key === 'Home' || key === 'End') {
|
||||||
|
desc.anchorOffset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSelection = selection.merge(desc) as SelectionState;
|
||||||
|
setAndTrackEditorState(EditorState.forceSelection(state, newSelection));
|
||||||
|
},
|
||||||
|
[editorStateRef, setAndTrackEditorState]
|
||||||
|
);
|
||||||
|
|
||||||
const handleEditorArrowKey = React.useCallback(
|
const handleEditorArrowKey = React.useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
|
latestKeyRef.current = e.key;
|
||||||
|
|
||||||
if (e.key === 'ArrowUp') {
|
if (e.key === 'ArrowUp') {
|
||||||
selectEmojiResult('prev', e);
|
selectEmojiResult('prev', e);
|
||||||
}
|
}
|
||||||
|
@ -462,8 +592,26 @@ export const CompositionInput = ({
|
||||||
if (e.key === 'ArrowDown') {
|
if (e.key === 'ArrowDown') {
|
||||||
selectEmojiResult('next', e);
|
selectEmojiResult('next', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowLeft' && e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.shiftKey) {
|
||||||
|
setCursor('Shift-Home');
|
||||||
|
} else {
|
||||||
|
setCursor('Home');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'ArrowRight' && e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.shiftKey) {
|
||||||
|
setCursor('Shift-End');
|
||||||
|
} else {
|
||||||
|
setCursor('End');
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[selectEmojiResult]
|
[latestKeyRef, selectEmojiResult, setCursor]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleEscapeKey = React.useCallback(
|
const handleEscapeKey = React.useCallback(
|
||||||
|
@ -503,24 +651,18 @@ export const CompositionInput = ({
|
||||||
.getLastCreatedEntityKey();
|
.getLastCreatedEntityKey();
|
||||||
const word = getWordAtCaret();
|
const word = getWordAtCaret();
|
||||||
|
|
||||||
let newContent = replaceWord
|
let newContent = Modifier.replaceText(
|
||||||
? Modifier.replaceText(
|
oldContent,
|
||||||
oldContent,
|
replaceWord
|
||||||
selection.merge({
|
? (selection.merge({
|
||||||
anchorOffset: word.start,
|
anchorOffset: word.start,
|
||||||
focusOffset: word.end,
|
focusOffset: word.end,
|
||||||
}) as SelectionState,
|
}) as SelectionState)
|
||||||
emojiContent,
|
: selection,
|
||||||
undefined,
|
emojiContent,
|
||||||
emojiEntityKey
|
undefined,
|
||||||
)
|
emojiEntityKey
|
||||||
: Modifier.insertText(
|
);
|
||||||
oldContent,
|
|
||||||
selection,
|
|
||||||
emojiContent,
|
|
||||||
undefined,
|
|
||||||
emojiEntityKey
|
|
||||||
);
|
|
||||||
|
|
||||||
const afterSelection = newContent.getSelectionAfter();
|
const afterSelection = newContent.getSelectionAfter();
|
||||||
|
|
||||||
|
@ -611,6 +753,15 @@ export const CompositionInput = ({
|
||||||
selectEmojiResult('prev');
|
selectEmojiResult('prev');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
command === 'Shift-End' ||
|
||||||
|
command === 'End' ||
|
||||||
|
command === 'Shift-Home' ||
|
||||||
|
command === 'Home'
|
||||||
|
) {
|
||||||
|
setCursor(command);
|
||||||
|
}
|
||||||
|
|
||||||
return 'not-handled';
|
return 'not-handled';
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
@ -619,6 +770,7 @@ export const CompositionInput = ({
|
||||||
resetEmojiResults,
|
resetEmojiResults,
|
||||||
selectEmojiResult,
|
selectEmojiResult,
|
||||||
setAndTrackEditorState,
|
setAndTrackEditorState,
|
||||||
|
setCursor,
|
||||||
skinTone,
|
skinTone,
|
||||||
submit,
|
submit,
|
||||||
]
|
]
|
||||||
|
@ -637,7 +789,10 @@ export const CompositionInput = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorKeybindingFn = React.useCallback(
|
const editorKeybindingFn = React.useCallback(
|
||||||
|
// tslint:disable-next-line cyclomatic-complexity
|
||||||
(e: React.KeyboardEvent): CompositionInputEditorCommand | null => {
|
(e: React.KeyboardEvent): CompositionInputEditorCommand | null => {
|
||||||
|
latestKeyRef.current = e.key;
|
||||||
|
|
||||||
if (e.key === 'Enter' && emojiResults.length > 0) {
|
if (e.key === 'Enter' && emojiResults.length > 0) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -654,6 +809,30 @@ export const CompositionInput = ({
|
||||||
return 'submit';
|
return 'submit';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey && e.key === 'End') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
return 'Shift-End';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'End') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
return 'End';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.shiftKey && e.key === 'Home') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
return 'Shift-Home';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Home') {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
return 'Home';
|
||||||
|
}
|
||||||
|
|
||||||
if (e.key === 'n' && e.ctrlKey) {
|
if (e.key === 'n' && e.ctrlKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
|
@ -680,7 +859,7 @@ export const CompositionInput = ({
|
||||||
|
|
||||||
return getDefaultKeyBinding(e);
|
return getDefaultKeyBinding(e);
|
||||||
},
|
},
|
||||||
[emojiResults, large]
|
[latestKeyRef, emojiResults, large]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create popper root
|
// Create popper root
|
||||||
|
@ -732,6 +911,7 @@ export const CompositionInput = ({
|
||||||
setAndTrackEditorState(
|
setAndTrackEditorState(
|
||||||
EditorState.forceSelection(oldState, oldState.getSelection())
|
EditorState.forceSelection(oldState, oldState.getSelection())
|
||||||
);
|
);
|
||||||
|
onFocus();
|
||||||
};
|
};
|
||||||
|
|
||||||
rootEl.addEventListener('focusin', onFocusIn);
|
rootEl.addEventListener('focusin', onFocusIn);
|
||||||
|
@ -743,7 +923,7 @@ export const CompositionInput = ({
|
||||||
|
|
||||||
return noop;
|
return noop;
|
||||||
},
|
},
|
||||||
[editorStateRef, rootElRef, setAndTrackEditorState]
|
[editorStateRef, onFocus, rootElRef, setAndTrackEditorState]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (inputApi) {
|
if (inputApi) {
|
||||||
|
@ -780,6 +960,8 @@ export const CompositionInput = ({
|
||||||
placeholder={i18n('sendMessage')}
|
placeholder={i18n('sendMessage')}
|
||||||
onUpArrow={handleEditorArrowKey}
|
onUpArrow={handleEditorArrowKey}
|
||||||
onDownArrow={handleEditorArrowKey}
|
onDownArrow={handleEditorArrowKey}
|
||||||
|
onLeftArrow={handleEditorArrowKey}
|
||||||
|
onRightArrow={handleEditorArrowKey}
|
||||||
onEscape={handleEscapeKey}
|
onEscape={handleEscapeKey}
|
||||||
onTab={onTab}
|
onTab={onTab}
|
||||||
handleKeyCommand={handleEditorCommand}
|
handleKeyCommand={handleEditorCommand}
|
||||||
|
|
|
@ -38,6 +38,11 @@ export type EmojiSkinVariation = {
|
||||||
has_img_messenger: boolean;
|
has_img_messenger: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DataFromEmojiText = {
|
||||||
|
shortName: string;
|
||||||
|
tone?: SkinToneKey;
|
||||||
|
};
|
||||||
|
|
||||||
export type EmojiData = {
|
export type EmojiData = {
|
||||||
name: string;
|
name: string;
|
||||||
unified: string;
|
unified: string;
|
||||||
|
@ -116,6 +121,7 @@ export const preloadImages = async () => {
|
||||||
|
|
||||||
const dataByShortName = keyBy(data, 'short_name');
|
const dataByShortName = keyBy(data, 'short_name');
|
||||||
const imageByEmoji: { [key: string]: string } = {};
|
const imageByEmoji: { [key: string]: string } = {};
|
||||||
|
const dataByEmoji: { [key: string]: DataFromEmojiText } = {};
|
||||||
|
|
||||||
export const dataByCategory = mapValues(
|
export const dataByCategory = mapValues(
|
||||||
groupBy(data, ({ category }) => {
|
groupBy(data, ({ category }) => {
|
||||||
|
@ -242,20 +248,8 @@ export function emojiToImage(emoji: string): string | undefined {
|
||||||
return imageByEmoji[emoji];
|
return imageByEmoji[emoji];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceColons(str: string) {
|
export function emojiToData(emoji: string): DataFromEmojiText | undefined {
|
||||||
return str.replace(/:[a-z0-9-_+]+:(?::skin-tone-[1-5]:)?/gi, m => {
|
return dataByEmoji[emoji];
|
||||||
const [shortName = '', skinTone = '0'] = m
|
|
||||||
.replace('skin-tone-', '')
|
|
||||||
.toLowerCase()
|
|
||||||
.split(':')
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
if (shortName && isShortName(shortName)) {
|
|
||||||
return convertShortName(shortName, parseInt(skinTone, 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
return m;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCountOfAllMatches(str: string, regex: RegExp) {
|
function getCountOfAllMatches(str: string, regex: RegExp) {
|
||||||
|
@ -305,12 +299,17 @@ data.forEach(emoji => {
|
||||||
}
|
}
|
||||||
|
|
||||||
imageByEmoji[convertShortName(short_name)] = makeImagePath(image);
|
imageByEmoji[convertShortName(short_name)] = makeImagePath(image);
|
||||||
|
dataByEmoji[convertShortName(short_name)] = { shortName: short_name };
|
||||||
|
|
||||||
if (skin_variations) {
|
if (skin_variations) {
|
||||||
Object.entries(skin_variations).forEach(([tone, variation]) => {
|
Object.entries(skin_variations).forEach(([tone, variation]) => {
|
||||||
imageByEmoji[
|
imageByEmoji[
|
||||||
convertShortName(short_name, tone as SkinToneKey)
|
convertShortName(short_name, tone as SkinToneKey)
|
||||||
] = makeImagePath(variation.image);
|
] = makeImagePath(variation.image);
|
||||||
|
dataByEmoji[convertShortName(short_name, tone as SkinToneKey)] = {
|
||||||
|
shortName: short_name,
|
||||||
|
tone: tone as SkinToneKey,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { assert } from 'chai';
|
|
||||||
|
|
||||||
import { replaceColons } from '../../../components/emoji/lib';
|
|
||||||
|
|
||||||
describe('replaceColons', () => {
|
|
||||||
it('replaces known emoji short names between colons', () => {
|
|
||||||
const anEmoji = replaceColons('hello :grinning:');
|
|
||||||
assert.equal(anEmoji, 'hello 😀');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('understands skin tone modifiers', () => {
|
|
||||||
const skinToneModifierEmoji = replaceColons('hello :wave::skin-tone-5:!');
|
|
||||||
assert.equal(skinToneModifierEmoji, 'hello 👋🏿!');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes through strings with no colons', () => {
|
|
||||||
const noEmoji = replaceColons('hello');
|
|
||||||
assert.equal(noEmoji, 'hello');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('ignores unknown emoji', () => {
|
|
||||||
const unknownEmoji = replaceColons(':Unknown: :unknown:');
|
|
||||||
assert.equal(unknownEmoji, ':Unknown: :unknown:');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('converts short names to lowercase before matching them', () => {
|
|
||||||
const emojiWithCaps = replaceColons('hello :Grinning:');
|
|
||||||
assert.equal(emojiWithCaps, 'hello 😀');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -231,9 +231,9 @@
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/modules/emojis.js",
|
"path": "js/modules/emojis.js",
|
||||||
"line": "async function load() {",
|
"line": "async function load() {",
|
||||||
"lineNumber": 13,
|
"lineNumber": 11,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2019-05-23T22:27:53.554Z"
|
"updated": "2019-10-31T17:28:08.684Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue