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

Replace plain text emoji at the end of a line #6784

Merged
merged 6 commits into from
Sep 14, 2021
Merged
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
30 changes: 20 additions & 10 deletions src/components/views/rooms/BasicMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from
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 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;

Expand Down Expand Up @@ -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 range = model.startRange(caretPosition);
// expand range max 8 characters backwards from caretPosition,
Expand All @@ -170,28 +171,33 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
range.expandBackwardsWhile((index, offset) => {
const part = model.parts[index];
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) {
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] === " ";
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
// 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));
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
// 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 => {
renderModel(this.editorRef.current, this.props.model);
Expand Down Expand Up @@ -607,8 +613,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
};

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

private configureShouldShowPillAvatar = (): void => {
Expand All @@ -621,6 +626,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
this.setState({ surroundWith });
};

private transform = (documentPosition: DocumentPosition): void => {
const shouldReplace = SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji');
if (shouldReplace) this.replaceEmoticon(documentPosition, REGEX_EMOTICON_WHITESPACE);
};

componentWillUnmount() {
document.removeEventListener("selectionchange", this.onSelectionChange);
this.editorRef.current.removeEventListener("input", this.onInput, true);
Expand Down
23 changes: 16 additions & 7 deletions src/components/views/rooms/SendMessageComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ import {
textSerialize,
unescapeMessage,
} from '../../../editor/serialize';
import BasicMessageComposer, { REGEX_EMOTICON } from "./BasicMessageComposer";
import { CommandPartCreator, Part, PartCreator, SerializedPart, Type } from '../../../editor/parts';
import BasicMessageComposer from "./BasicMessageComposer";
import ReplyThread from "../elements/ReplyThread";
import { findEditableEvent } from '../../../utils/EventUtils';
import SendHistoryManager from "../../../SendHistoryManager";
Expand Down Expand Up @@ -347,15 +347,24 @@ export default class SendMessageComposer extends React.Component<IProps> {
}

public async sendMessage(): Promise<void> {
if (this.model.isEmpty) {
const model = this.model;

if (model.isEmpty) {
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;
let shouldSend = true;
let content;

if (!containsEmote(this.model) && this.isSlashCommand()) {
if (!containsEmote(model) && this.isSlashCommand()) {
const [cmd, args, commandText] = this.getSlashCommand();
if (cmd) {
if (cmd.category === CommandCategories.messages) {
Expand Down Expand Up @@ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
}
}

if (isQuickReaction(this.model)) {
if (isQuickReaction(model)) {
shouldSend = false;
this.sendQuickReaction();
}
Expand All @@ -410,7 +419,7 @@ export default class SendMessageComposer extends React.Component<IProps> {
const { roomId } = this.props.room;
if (!content) {
content = createMessageContent(
this.model,
model,
replyToEvent,
this.props.replyInThread,
this.props.permalinkCreator,
Expand Down Expand Up @@ -446,9 +455,9 @@ export default class SendMessageComposer extends React.Component<IProps> {
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, !!replyToEvent, content);
}

this.sendHistoryManager.save(this.model, replyToEvent);
this.sendHistoryManager.save(model, replyToEvent);
// clear composer
this.model.reset([]);
model.reset([]);
this.editorRef.current?.clearUndoHistory();
this.editorRef.current?.focus();
this.clearStoredEditorState();
Expand Down
9 changes: 8 additions & 1 deletion src/editor/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,20 @@ export default class Range {
this._end = bIsLarger ? positionB : positionA;
}

public moveStart(delta: number): void {
public moveStartForwards(delta: number): void {
this._start = this._start.forwardsWhile(this.model, () => {
delta -= 1;
return delta >= 0;
});
}

public moveEndBackwards(delta: number): void {
this._end = this._end.backwardsWhile(this.model, () => {
delta -= 1;
return delta >= 0;
});
}

public trim(): void {
this._start = this._start.forwardsWhile(this.model, whitespacePredicate);
this._end = this._end.backwardsWhile(this.model, whitespacePredicate);
Expand Down