From 5d6c2d94ffeb60d6d5c47d08a93d65315068b154 Mon Sep 17 00:00:00 2001 From: audric Date: Wed, 4 Aug 2021 14:56:00 +1000 Subject: [PATCH] fix emoji being inserted into mentions identifier if the cursor is before the first mention => insert it correctly if the cursor is after the last mention => insert it correctly if the cursor is between those two => insert it at the end of the composition box --- ts/components/MessageBodyHighlight.tsx | 94 +++++++++---------- ts/components/conversation/Emojify.tsx | 1 + ts/components/conversation/Message.tsx | 2 - .../conversation/SessionCompositionBox.tsx | 65 ++++++++++++- .../conversation/SessionEmojiPanel.tsx | 55 ++++------- 5 files changed, 129 insertions(+), 88 deletions(-) diff --git a/ts/components/MessageBodyHighlight.tsx b/ts/components/MessageBodyHighlight.tsx index 2bf6e39ec..f42cfe1f8 100644 --- a/ts/components/MessageBodyHighlight.tsx +++ b/ts/components/MessageBodyHighlight.tsx @@ -8,9 +8,9 @@ import { SizeClassType } from '../util/emoji'; import { RenderTextCallbackType } from '../types/Util'; -interface Props { +type Props = { text: string; -} +}; const renderNewLines: RenderTextCallbackType = ({ text, key }) => ( @@ -28,56 +28,27 @@ const renderEmoji = ({ renderNonEmoji: RenderTextCallbackType; }) => ; -export class MessageBodyHighlight extends React.Component { - public render() { - const { text } = this.props; - const results: Array = []; - const FIND_BEGIN_END = /<>(.+?)<>/g; +export const MessageBodyHighlight = (props: Props) => { + const { text } = props; + const results: Array = []; + const FIND_BEGIN_END = /<>(.+?)<>/g; - let match = FIND_BEGIN_END.exec(text); - let last = 0; - let count = 1; + let match = FIND_BEGIN_END.exec(text); + let last = 0; + let count = 1; - if (!match) { - return ; - } - - const sizeClass = ''; - - while (match) { - if (last < match.index) { - const beforeText = text.slice(last, match.index); - results.push( - renderEmoji({ - text: beforeText, - sizeClass, - key: count++, - renderNonEmoji: renderNewLines, - }) - ); - } - - const [, toHighlight] = match; - results.push( - - {renderEmoji({ - text: toHighlight, - sizeClass, - key: count++, - renderNonEmoji: renderNewLines, - })} - - ); + if (!match) { + return ; + } - // @ts-ignore - last = FIND_BEGIN_END.lastIndex; - match = FIND_BEGIN_END.exec(text); - } + const sizeClass = ''; - if (last < text.length) { + while (match) { + if (last < match.index) { + const beforeText = text.slice(last, match.index); results.push( renderEmoji({ - text: text.slice(last), + text: beforeText, sizeClass, key: count++, renderNonEmoji: renderNewLines, @@ -85,6 +56,33 @@ export class MessageBodyHighlight extends React.Component { ); } - return results; + const [, toHighlight] = match; + results.push( + + {renderEmoji({ + text: toHighlight, + sizeClass, + key: count++, + renderNonEmoji: renderNewLines, + })} + + ); + + // @ts-ignore + last = FIND_BEGIN_END.lastIndex; + match = FIND_BEGIN_END.exec(text); } -} + + if (last < text.length) { + results.push( + renderEmoji({ + text: text.slice(last), + sizeClass, + key: count++, + renderNonEmoji: renderNewLines, + }) + ); + } + + return results; +}; diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx index 9fa41a70d..5a83619c9 100644 --- a/ts/components/conversation/Emojify.tsx +++ b/ts/components/conversation/Emojify.tsx @@ -43,6 +43,7 @@ export class Emojify extends React.Component { while (match) { if (last < match.index) { const textWithNoEmoji = text.slice(last, match.index); + results.push( renderNonEmoji({ text: textWithNoEmoji, diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index a60dfc8df..221a0e7b7 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -860,8 +860,6 @@ class MessageInner extends React.PureComponent { if (target.className === 'text-selectable' || window.contextMenuShown) { return; } - event.preventDefault(); - event.stopPropagation(); } } diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index d831f2c5b..4465b2208 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -964,6 +964,63 @@ class SessionCompositionBoxInner extends React.Component { this.setState({ message }); } + private getSelectionBasedOnMentions(index: number) { + // we have to get the real selectionStart/end of an index in the mentions box. + // this is kind of a pain as the mentions box has two inputs, one with the real text, and one with the extracted mentions + + // the index shown to the user is actually just the visible part of the mentions (so the part between ᅲ...ᅭ + const matches = this.state.message.match(this.mentionsRegex); + + let lastMatchStartIndex = 0; + let lastMatchEndIndex = 0; + let lastRealMatchEndIndex = 0; + + if (!matches) { + return index; + } + const mapStartToLengthOfMatches = matches.map(match => { + const displayNameStart = match.indexOf('\uFFD7') + 1; + const displayNameEnd = match.lastIndexOf('\uFFD2'); + const displayName = match.substring(displayNameStart, displayNameEnd); + + const currentMatchStartIndex = this.state.message.indexOf(match) + lastMatchStartIndex; + lastMatchStartIndex = currentMatchStartIndex; + lastMatchEndIndex = currentMatchStartIndex + match.length; + + const realLength = displayName.length + 1; + lastRealMatchEndIndex = lastRealMatchEndIndex + realLength; + + // the +1 is for the @ + return { + length: displayName.length + 1, + lastRealMatchEndIndex, + start: lastMatchStartIndex, + end: lastMatchEndIndex, + }; + }); + + const beforeFirstMatch = index < mapStartToLengthOfMatches[0].start; + if (beforeFirstMatch) { + // those first char are always just char, so the mentions logic does not come into account + return index; + } + const lastMatchMap = _.last(mapStartToLengthOfMatches); + + if (!lastMatchMap) { + return Number.MAX_SAFE_INTEGER; + } + + const indexIsAfterEndOfLastMatch = lastMatchMap.lastRealMatchEndIndex <= index; + if (indexIsAfterEndOfLastMatch) { + const lastEnd = lastMatchMap.end; + const diffBetweenEndAndLastRealEnd = index - lastMatchMap.lastRealMatchEndIndex; + return lastEnd + diffBetweenEndAndLastRealEnd - 1; + } + // now this is the hard part, the cursor is currently between the end of the first match and the start of the last match + // for now, just append it to the end + return Number.MAX_SAFE_INTEGER; + } + private onEmojiClick({ colons }: { colons: string }) { const messageBox = this.textarea.current; if (!messageBox) { @@ -973,10 +1030,12 @@ class SessionCompositionBoxInner extends React.Component { const { message } = this.state; const currentSelectionStart = Number(messageBox.selectionStart); - const currentSelectionEnd = Number(messageBox.selectionEnd); - const before = message.slice(0, currentSelectionStart); - const end = message.slice(currentSelectionEnd); + const realSelectionStart = this.getSelectionBasedOnMentions(currentSelectionStart); + + const before = message.slice(0, realSelectionStart); + const end = message.slice(realSelectionStart); + const newMessage = `${before}${colons}${end}`; this.setState({ message: newMessage }, () => { diff --git a/ts/components/session/conversation/SessionEmojiPanel.tsx b/ts/components/session/conversation/SessionEmojiPanel.tsx index 9657c15ca..b9b174712 100644 --- a/ts/components/session/conversation/SessionEmojiPanel.tsx +++ b/ts/components/session/conversation/SessionEmojiPanel.tsx @@ -3,42 +3,27 @@ import classNames from 'classnames'; import { Picker } from 'emoji-mart'; import { Constants } from '../../../session'; -interface Props { +type Props = { onEmojiClicked: (emoji: any) => void; show: boolean; -} +}; -interface State { - // FIXME Use Emoji-Mart categories - category: null; -} +export const SessionEmojiPanel = (props: Props) => { + const { onEmojiClicked, show } = props; -export class SessionEmojiPanel extends React.Component { - constructor(props: Props) { - super(props); - - this.state = { - category: null, - }; - } - - public render() { - const { onEmojiClicked, show } = this.props; - - return ( -
- './images/emoji/emoji-sheet-twitter-32.png'} - set={'twitter'} - sheetSize={32} - darkMode={true} - color={Constants.UI.COLORS.GREEN} - showPreview={true} - title={''} - onSelect={onEmojiClicked} - autoFocus={true} - /> -
- ); - } -} + return ( +
+ './images/emoji/emoji-sheet-twitter-32.png'} + set={'twitter'} + sheetSize={32} + darkMode={true} + color={Constants.UI.COLORS.GREEN} + showPreview={true} + title={''} + onSelect={onEmojiClicked} + autoFocus={true} + /> +
+ ); +};