Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Replace emoticons when sending a message rather than when typing #6718

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 0 additions & 47 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import classNames from 'classnames';
import React, { createRef, ClipboardEvent } from 'react';
import { Room } from 'matrix-js-sdk/src/models/room';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';

import EditorModel from '../../../editor/model';
import HistoryManager from '../../../editor/history';
Expand All @@ -37,7 +36,6 @@ import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
import SettingsStore from "../../../settings/SettingsStore";
import { Key } from "../../../Keyboard";
import { EMOTICON_TO_EMOJI } from "../../../emoji";
import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands";
import Range from "../../../editor/range";
import MessageComposerFormatBar, { Formatting } from "./MessageComposerFormatBar";
Expand All @@ -49,9 +47,6 @@ import { ICompletion } from "../../../autocomplete/Autocompleter";
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
import { replaceableComponent } from "../../../utils/replaceableComponent";

// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');

const IS_MAC = navigator.platform.indexOf("Mac") !== -1;

const SURROUND_WITH_CHARACTERS = ["\"", "_", "`", "'", "*", "~", "$"];
Expand Down Expand Up @@ -123,7 +118,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private lastCaret: DocumentOffset;
private lastSelection: ReturnType<typeof cloneSelection>;

private readonly emoticonSettingHandle: string;
private readonly shouldShowPillAvatarSettingHandle: string;
private readonly surroundWithHandle: string;
private readonly historyManager = new HistoryManager();
Expand All @@ -136,9 +130,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
showVisualBell: false,
};

this.emoticonSettingHandle = SettingsStore.watchSetting('MessageComposerInput.autoReplaceEmoji', null,
this.configureEmoticonAutoReplace);
this.configureEmoticonAutoReplace();
this.shouldShowPillAvatarSettingHandle = SettingsStore.watchSetting("Pill.shouldShowPillAvatar", null,
this.configureShouldShowPillAvatar);
this.surroundWithHandle = SettingsStore.watchSetting("MessageComposerInput.surroundWith", null,
Expand All @@ -161,38 +152,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}
}

private replaceEmoticon = (caretPosition: DocumentPosition): number => {
const { model } = this.props;
const range = model.startRange(caretPosition);
// expand range max 8 characters backwards from caretPosition,
// as a space to look for an emoticon
let n = 8;
range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index];
n -= 1;
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate);
});
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text);
if (emoticonMatch) {
const query = emoticonMatch[1].replace("-", "");
// try both exact match and lower-case, this means that xd won't match xD but :P will match :p
const data = EMOTICON_TO_EMOJI.get(query) || EMOTICON_TO_EMOJI.get(query.toLowerCase());

if (data) {
const { partCreator } = model;
const hasPrecedingSpace = emoticonMatch[0][0] === " ";
// we need the range to only comprise of the emoticon
// because we'll replace the whole range with an emoji,
// so move the start forward to the start of the emoticon.
// Take + 1 because index is reported without the possible preceding space.
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0));
// this returns the amount of added/removed characters during the replace
// so the caret position can be adjusted.
return range.replace([partCreator.plain(data.unicode + " ")]);
}
}
};

private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
renderModel(this.editorRef.current, this.props.model);
if (selection) { // set the caret/selection
Expand Down Expand Up @@ -606,11 +565,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.setState({ completionIndex });
};

private configureEmoticonAutoReplace = (): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
};

private configureShouldShowPillAvatar = (): void => {
const showPillAvatar = SettingsStore.getValue("Pill.shouldShowPillAvatar");
this.setState({ showPillAvatar });
Expand All @@ -626,7 +580,6 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.editorRef.current.removeEventListener("input", this.onInput, true);
this.editorRef.current.removeEventListener("compositionstart", this.onCompositionStart, true);
this.editorRef.current.removeEventListener("compositionend", this.onCompositionEnd, true);
SettingsStore.unwatchSetting(this.emoticonSettingHandle);
SettingsStore.unwatchSetting(this.shouldShowPillAvatarSettingHandle);
SettingsStore.unwatchSetting(this.surroundWithHandle);
}
Expand Down
22 changes: 20 additions & 2 deletions src/editor/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,14 @@ import SettingsStore from '../settings/SettingsStore';
import SdkConfig from '../SdkConfig';
import cheerio from 'cheerio';
import { Type } from './parts';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
import { EMOTICON_TO_EMOJI } from "../emoji";

// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp("(?<=^|\\s)("+ EMOTICON_REGEX.source +")(?=\\s|$)", "g");

export function mdSerialize(model: EditorModel): string {
return model.parts.reduce((html, part) => {
const md = model.parts.reduce((html, part) => {
switch (part.type) {
case Type.Newline:
return html + "\n";
Expand All @@ -44,6 +49,8 @@ export function mdSerialize(model: EditorModel): string {
`[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
}
}, "");

return replaceEmoticonsIfNeeded(md);
}

export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false } = {}): string {
Expand Down Expand Up @@ -158,7 +165,7 @@ export function htmlSerializeIfNeeded(model: EditorModel, { forceHTML = false }
}

export function textSerialize(model: EditorModel): string {
return model.parts.reduce((text, part) => {
const text = model.parts.reduce((text, part) => {
switch (part.type) {
case Type.Newline:
return text + "\n";
Expand All @@ -175,6 +182,8 @@ export function textSerialize(model: EditorModel): string {
return text + `${part.text}`;
}
}, "");

return replaceEmoticonsIfNeeded(text);
}

export function containsEmote(model: EditorModel): boolean {
Expand Down Expand Up @@ -217,3 +226,12 @@ export function unescapeMessage(model: EditorModel): EditorModel {
}
return model;
}

export function replaceEmoticonsIfNeeded(str: string): string {
if (!SettingsStore.getValue("MessageComposerInput.autoReplaceEmoji")) return str;

return str.replace(REGEX_EMOTICON_WHITESPACE, (substring: string) => {
// Try both exact match and lower-case, this means that xd won't match xD but :P will match :p
return (EMOTICON_TO_EMOJI.get(substring) || EMOTICON_TO_EMOJI.get(substring.toLowerCase()))?.unicode;
});
}