105 lines
2.9 KiB
TypeScript
105 lines
2.9 KiB
TypeScript
|
import React from 'react';
|
||
|
import { Emojify } from './Emojify';
|
||
|
import { 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={`module-message-body__at-mention module-message-body__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"
|
||
|
>
|
||
|
@
|
||
|
<Emojify text={range.replacementText} />
|
||
|
</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;
|
||
|
}
|
||
|
|
||
|
return bodyRanges.reduce((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);
|
||
|
};
|