Merge pull request #6784 from SimonBrandner/fix/end-of-line-emoji
Replace plain text emoji at the end of a linepull/21833/head
						commit
						7b9dc09cd4
					
				|  | @ -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; | ||||
| 
 | ||||
|  | @ -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,
 | ||||
|  | @ -170,9 +171,9 @@ 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
 | ||||
|  | @ -180,18 +181,23 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> | |||
| 
 | ||||
|             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); | ||||
|  | @ -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 => { | ||||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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"; | ||||
|  | @ -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) { | ||||
|  | @ -400,7 +409,7 @@ export default class SendMessageComposer extends React.Component<IProps> { | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (isQuickReaction(this.model)) { | ||||
|         if (isQuickReaction(model)) { | ||||
|             shouldSend = false; | ||||
|             this.sendQuickReaction(); | ||||
|         } | ||||
|  | @ -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, | ||||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Travis Ralston
						Travis Ralston