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
This commit is contained in:
audric 2021-08-04 14:56:00 +10:00
parent b05910e219
commit 5d6c2d94ff
5 changed files with 129 additions and 88 deletions

View File

@ -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 }) => (
<AddNewLines key={key} text={text} />
@ -28,56 +28,27 @@ const renderEmoji = ({
renderNonEmoji: RenderTextCallbackType;
}) => <Emojify key={key} text={text} sizeClass={sizeClass} renderNonEmoji={renderNonEmoji} />;
export class MessageBodyHighlight extends React.Component<Props> {
public render() {
const { text } = this.props;
const results: Array<any> = [];
const FIND_BEGIN_END = /<<left>>(.+?)<<right>>/g;
export const MessageBodyHighlight = (props: Props) => {
const { text } = props;
const results: Array<any> = [];
const FIND_BEGIN_END = /<<left>>(.+?)<<right>>/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 <MessageBody disableJumbomoji={true} disableLinks={true} text={text} />;
}
if (!match) {
return <MessageBody disableJumbomoji={true} disableLinks={true} text={text} />;
}
const sizeClass = '';
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(
<span className="module-message-body__highlight" key={count++}>
{renderEmoji({
text: toHighlight,
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
})}
</span>
);
// @ts-ignore
last = FIND_BEGIN_END.lastIndex;
match = FIND_BEGIN_END.exec(text);
}
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<Props> {
);
}
return results;
const [, toHighlight] = match;
results.push(
<span className="module-message-body__highlight" key={count++}>
{renderEmoji({
text: toHighlight,
sizeClass,
key: count++,
renderNonEmoji: renderNewLines,
})}
</span>
);
// @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;
};

View File

@ -43,6 +43,7 @@ export class Emojify extends React.Component<Props> {
while (match) {
if (last < match.index) {
const textWithNoEmoji = text.slice(last, match.index);
results.push(
renderNonEmoji({
text: textWithNoEmoji,

View File

@ -860,8 +860,6 @@ class MessageInner extends React.PureComponent<Props, State> {
if (target.className === 'text-selectable' || window.contextMenuShown) {
return;
}
event.preventDefault();
event.stopPropagation();
}
}

View File

@ -964,6 +964,63 @@ class SessionCompositionBoxInner extends React.Component<Props, State> {
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<Props, State> {
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 }, () => {

View File

@ -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<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
category: null,
};
}
public render() {
const { onEmojiClicked, show } = this.props;
return (
<div className={classNames('session-emoji-panel', show && 'show')}>
<Picker
backgroundImageFn={() => './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}
/>
</div>
);
}
}
return (
<div className={classNames('session-emoji-panel', show && 'show')}>
<Picker
backgroundImageFn={() => './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}
/>
</div>
);
};