// Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only 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( { 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" > @ ); } 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 , 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); };