Linkify untruncated text

This commit is contained in:
Fedor Indutny 2025-06-10 16:13:42 -07:00 committed by GitHub
parent 8ea030074e
commit 6297e12803
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 48 additions and 6 deletions

View file

@ -704,6 +704,7 @@ export function StoryViewer({
onExpandSpoiler={data => setIsSpoilerExpanded(data)}
renderLocation={RenderLocation.StoryViewer}
text={caption.text}
originalText={caption.text}
/>
{caption.hasReadMore && !hasExpandedCaption && (
<button

View file

@ -40,6 +40,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showConversation:
overrideProps.showConversation || action('showConversation'),
text: overrideProps.text || '',
originalText: overrideProps.originalText || overrideProps.text || '',
textAttachment: overrideProps.textAttachment || {
pending: false,
},
@ -525,3 +526,15 @@ export function ZalgoText(): JSX.Element {
return <MessageBody {...props} />;
}
export function LinkOverReadMoreBoundary(): JSX.Element {
const text = 'https://hello.me';
const originalText = 'https://hello.me123';
const props = createProps({
text,
originalText,
});
return <MessageBody {...props} />;
}

View file

@ -60,6 +60,7 @@ export type Props = {
AttachmentType,
'pending' | 'digest' | 'key' | 'wasTooBig' | 'path'
>;
originalText: string;
};
/**
@ -84,8 +85,10 @@ export function MessageBody({
showConversation,
text,
textAttachment,
originalText,
}: Props): JSX.Element {
const shouldDisableLinks = disableLinks || !shouldLinkifyMessage(text);
const shouldDisableLinks =
disableLinks || !shouldLinkifyMessage(originalText);
const textWithSuffix =
textAttachment?.pending || onIncreaseTextLength || textAttachment?.wasTooBig
? `${text}...`
@ -177,6 +180,7 @@ export function MessageBody({
i18n={i18n}
isSpoilerExpanded={isSpoilerExpanded}
messageText={textWithSuffix}
originalMessageText={originalText}
onMentionTrigger={conversationId =>
showConversation?.({ conversationId })
}

View file

@ -83,6 +83,7 @@ export function MessageBodyReadMore({
showConversation={showConversation}
text={slicedText}
textAttachment={textAttachment}
originalText={text}
/>
);
}

View file

@ -45,6 +45,7 @@ type Props = {
i18n: LocalizerType;
isSpoilerExpanded: Record<number, boolean>;
messageText: string;
originalMessageText: string;
onExpandSpoiler?: (data: Record<number, boolean>) => void;
onMentionTrigger: (conversationId: string) => void;
renderLocation: RenderLocation;
@ -64,9 +65,12 @@ export function MessageTextRenderer({
onMentionTrigger,
renderLocation,
textLength,
originalMessageText,
}: Props): JSX.Element {
const finalNodes = React.useMemo(() => {
const links = disableLinks ? [] : extractLinks(messageText);
const links = disableLinks
? []
: extractLinks(messageText, originalMessageText);
// We need mentions to come last; they can't have children for proper rendering
const sortedRanges = sortBy(bodyRanges, range =>
@ -104,7 +108,7 @@ export function MessageTextRenderer({
// Group all contigusous spoilers to create one parent spoiler element in the DOM
return groupContiguousSpoilers(nodes);
}, [bodyRanges, disableLinks, messageText, textLength]);
}, [bodyRanges, disableLinks, messageText, originalMessageText, textLength]);
return (
<>
@ -424,19 +428,34 @@ function renderText({
}
export function extractLinks(
messageText: string
messageText: string,
// Full, untruncated message text
originalMessageText: string
): ReadonlyArray<BodyRange<{ url: string }>> {
// to support emojis immediately before links
// we replace emojis with a space for each byte
const matches = linkify.match(
messageText.replace(EMOJI_REGEXP, s => ' '.repeat(s.length))
originalMessageText.replace(EMOJI_REGEXP, s => ' '.repeat(s.length))
);
if (matches == null) {
return [];
}
return matches.map(match => {
// Only return matches present in the `messageText`
const currentMatches = matches.filter(({ index, lastIndex, url }) => {
if (index >= messageText.length) {
return false;
}
if (lastIndex > messageText.length) {
return false;
}
return messageText.slice(index, lastIndex) === url;
});
return currentMatches.map(match => {
return {
start: match.index,
length: match.lastIndex - match.index,

View file

@ -378,6 +378,7 @@ export function Quote(props: Props): JSX.Element | null {
isSpoilerExpanded={EMPTY_OBJECT}
renderLocation={RenderLocation.Quote}
text={text}
originalText={text}
/>
</div>
);

View file

@ -170,6 +170,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
prefix={draftPreview.prefix}
renderLocation={RenderLocation.ConversationList}
text={draftPreview.text}
originalText={draftPreview.text}
/>
</>
);
@ -191,6 +192,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
prefix={lastMessage.prefix}
renderLocation={RenderLocation.ConversationList}
text={lastMessage.text}
originalText={lastMessage.text}
/>
);
if (lastMessage.status) {

View file

@ -168,6 +168,7 @@ export const MessageSearchResult: FunctionComponent<PropsType> = React.memo(
const messageText = (
<MessageTextRenderer
messageText={cleanedSnippet}
originalMessageText={cleanedSnippet}
bodyRanges={displayBodyRanges}
direction={undefined}
disableLinks