signal-desktop/ts/components/conversation/AtMentionify.tsx
2021-10-26 14:15:33 -05:00

113 lines
3.2 KiB
TypeScript

// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { sortBy } from 'lodash';
import { Emojify } from './Emojify';
import type { BodyRangesType } from '../../types/Util';
export type Props = {
bodyRanges?: BodyRangesType;
direction?: 'incoming' | 'outgoing';
openConversation?: (conversationId: string, messageId?: string) => void;
text: string;
};
export const AtMentionify = ({
bodyRanges,
direction,
openConversation,
text,
}: Props): JSX.Element => {
if (!bodyRanges) {
return <>{text}</>;
}
const MENTIONS_REGEX = /(\uFFFC@(\d+))/g;
let match = MENTIONS_REGEX.exec(text);
let last = 0;
const rangeStarts = new Map();
bodyRanges.forEach(range => {
rangeStarts.set(range.start, range);
});
const results = [];
while (match) {
if (last < match.index) {
const textWithNoMentions = text.slice(last, match.index);
results.push(textWithNoMentions);
}
const rangeStart = Number(match[2]);
const range = rangeStarts.get(rangeStart);
if (range) {
results.push(
<span
className={`MessageBody__at-mention MessageBody__at-mention--${direction}`}
key={range.start}
onClick={() => {
if (openConversation && range.conversationID) {
openConversation(range.conversationID);
}
}}
onKeyUp={e => {
if (
e.target === e.currentTarget &&
e.keyCode === 13 &&
openConversation &&
range.conversationID
) {
openConversation(range.conversationID);
}
}}
tabIndex={0}
role="link"
data-id={range.conversationID}
data-title={range.replacementText}
>
<bdi>
@
<Emojify text={range.replacementText} />
</bdi>
</span>
);
}
last = MENTIONS_REGEX.lastIndex;
match = MENTIONS_REGEX.exec(text);
}
if (last < text.length) {
results.push(text.slice(last));
}
return <>{results}</>;
};
// At-mentions need to be pre-processed before being pushed through the
// AtMentionify component, this is due to bodyRanges containing start+length
// values that operate on the raw string. The text has to be passed through
// other components before being rendered in the <MessageBody />, components
// such as Linkify, and Emojify. These components receive the text prop as a
// string, therefore we're unable to mark it up with DOM nodes prior to handing
// it off to them. This function will encode the "start" position into the text
// string so we can later pull it off when rendering the @mention.
AtMentionify.preprocessMentions = (
text: string,
bodyRanges?: BodyRangesType
): string => {
if (!bodyRanges || !bodyRanges.length) {
return text;
}
// Sorting by the start index to ensure that we always replace last -> first.
return sortBy(bodyRanges, 'start').reduceRight((str, range) => {
const textBegin = str.substr(0, range.start);
const encodedMention = `\uFFFC@${range.start}`;
const textEnd = str.substr(range.start + range.length, str.length);
return `${textBegin}${encodedMention}${textEnd}`;
}, text);
};