Merge pull request #6784 from SimonBrandner/fix/end-of-line-emoji

Replace plain text emoji at the end of a line
This commit is contained in:
Travis Ralston 2021-09-14 13:33:34 -06:00 committed by GitHub
commit 7b9dc09cd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 44 additions and 18 deletions

View file

@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
// matches emoticons which follow the start of a line or whitespace // matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$'); const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
export const REGEX_EMOTICON = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')$');
const IS_MAC = navigator.platform.indexOf("Mac") !== -1; const IS_MAC = navigator.platform.indexOf("Mac") !== -1;
@ -161,7 +162,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
} }
} }
private replaceEmoticon = (caretPosition: DocumentPosition): number => { public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number {
const { model } = this.props; const { model } = this.props;
const range = model.startRange(caretPosition); const range = model.startRange(caretPosition);
// expand range max 8 characters backwards from caretPosition, // expand range max 8 characters backwards from caretPosition,
@ -170,9 +171,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
range.expandBackwardsWhile((index, offset) => { range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index]; const part = model.parts[index];
n -= 1; n -= 1;
return n >= 0 && (part.type === Type.Plain || part.type === Type.PillCandidate); return n >= 0 && [Type.Plain, Type.PillCandidate, Type.Newline].includes(part.type);
}); });
const emoticonMatch = REGEX_EMOTICON_WHITESPACE.exec(range.text); const emoticonMatch = regex.exec(range.text);
if (emoticonMatch) { if (emoticonMatch) {
const query = emoticonMatch[1].replace("-", ""); 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 // try both exact match and lower-case, this means that xd won't match xD but :P will match :p
@ -180,18 +181,23 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
if (data) { if (data) {
const { partCreator } = model; const { partCreator } = model;
const hasPrecedingSpace = emoticonMatch[0][0] === " "; const moveStart = emoticonMatch[0][0] === " " ? 1 : 0;
const moveEnd = emoticonMatch[0].length - emoticonMatch.length - moveStart;
// we need the range to only comprise of the emoticon // we need the range to only comprise of the emoticon
// because we'll replace the whole range with an emoji, // because we'll replace the whole range with an emoji,
// so move the start forward to the start of the emoticon. // so move the start forward to the start of the emoticon.
// Take + 1 because index is reported without the possible preceding space. // Take + 1 because index is reported without the possible preceding space.
range.moveStart(emoticonMatch.index + (hasPrecedingSpace ? 1 : 0)); range.moveStartForwards(emoticonMatch.index + moveStart);
// and move end backwards so that we don't replace the trailing space/newline
range.moveEndBackwards(moveEnd);
// this returns the amount of added/removed characters during the replace // this returns the amount of added/removed characters during the replace
// so the caret position can be adjusted. // so the caret position can be adjusted.
return range.replace([partCreator.plain(data.unicode + " ")]); return range.replace([partCreator.plain(data.unicode)]);
}
} }
} }
};
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => { private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
renderModel(this.editorRef.current, this.props.model); renderModel(this.editorRef.current, this.props.model);
@ -607,8 +613,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}; };
private configureEmoticonAutoReplace = (): void => { private configureEmoticonAutoReplace = (): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji'); this.props.model.setTransformCallback(this.transform);
this.props.model.setTransformCallback(shouldReplace ? this.replaceEmoticon : null);
}; };
private configureShouldShowPillAvatar = (): void => { private configureShouldShowPillAvatar = (): void => {
@ -621,6 +626,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.setState({ surroundWith }); this.setState({ surroundWith });
}; };
private transform = (documentPosition: DocumentPosition): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
};
componentWillUnmount() { componentWillUnmount() {
document.removeEventListener("selectionchange", this.onSelectionChange); document.removeEventListener("selectionchange", this.onSelectionChange);
this.editorRef.current.removeEventListener("input", this.onInput, true); this.editorRef.current.removeEventListener("input", this.onInput, true);

View file

@ -31,8 +31,8 @@ import {
textSerialize, textSerialize,
unescapeMessage, unescapeMessage,
} from '../../../editor/serialize'; } from '../../../editor/serialize';
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts'; import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import ReplyThread from "../elements/ReplyThread"; import ReplyThread from "../elements/ReplyThread";
import { findEditableEvent } from '../../../utils/EventUtils'; import { findEditableEvent } from '../../../utils/EventUtils';
import SendHistoryManager from "../../../SendHistoryManager"; import SendHistoryManager from "../../../SendHistoryManager";
@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
} }
public async sendMessage(): Promise<void> { public async sendMessage(): Promise<void> {
if (this.model.isEmpty) { const model = this.model;
if (model.isEmpty) {
return; return;
} }
// Replace emoticon at the end of the message
if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
const caret = this.editorRef.current?.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
this.editorRef.current?.replaceEmoticon(position, REGEX_EMOTICON);
}
const replyToEvent = this.props.replyToEvent; const replyToEvent = this.props.replyToEvent;
let shouldSend = true; let shouldSend = true;
let content; let content;
if (!containsEmote(this.model) && this.isSlashCommand()) { if (!containsEmote(model) && this.isSlashCommand()) {
const [cmd, args, commandText] = this.getSlashCommand(); const [cmd, args, commandText] = this.getSlashCommand();
if (cmd) { if (cmd) {
if (cmd.category === CommandCategories.messages) { if (cmd.category === CommandCategories.messages) {
@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
} }
} }
if (isQuickReaction(this.model)) { if (isQuickReaction(model)) {
shouldSend = false; shouldSend = false;
this.sendQuickReaction(); this.sendQuickReaction();
} }
@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
const { roomId } = this.props.room; const { roomId } = this.props.room;
if (!content) { if (!content) {
content = createMessageContent( content = createMessageContent(
this.model, model,
replyToEvent, replyToEvent,
this.props.replyInThread, this.props.replyInThread,
this.props.permalinkCreator, this.props.permalinkCreator,
@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content); CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
} }
this.sendHistoryManager.save(this.model, replyToEvent); this.sendHistoryManager.save(model, replyToEvent);
// clear composer // clear composer
this.model.reset([]); model.reset([]);
this.editorRef.current?.clearUndoHistory(); this.editorRef.current?.clearUndoHistory();
this.editorRef.current?.focus(); this.editorRef.current?.focus();
this.clearStoredEditorState(); this.clearStoredEditorState();

View file

@ -32,13 +32,20 @@ export default class Range {
this._end = bIsLarger ? positionB : positionA; this._end = bIsLarger ? positionB : positionA;
} }
public moveStart(delta: number): void { public moveStartForwards(delta: number): void {
this._start = this._start.forwardsWhile(this.model, () => { this._start = this._start.forwardsWhile(this.model, () => {
delta -= 1; delta -= 1;
return delta >= 0; return delta >= 0;
}); });
} }
public moveEndBackwards(delta: number): void {
this._end = this._end.backwardsWhile(this.model, () => {
delta -= 1;
return delta >= 0;
});
}
public trim(): void { public trim(): void {
this._start = this._start.forwardsWhile(this.model, whitespacePredicate); this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
this._end = this._end.backwardsWhile(this.model, whitespacePredicate); this._end = this._end.backwardsWhile(this.model, whitespacePredicate);