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:
commit
7b9dc09cd4
3 changed files with 44 additions and 18 deletions
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue