diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index b215d8f87..f5cadfcd5 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -6,7 +6,7 @@ import React from 'react'; import LinkifyIt from 'linkify-it'; import type { RenderTextCallbackType } from '../../types/Util'; -import { isLinkSneaky } from '../../types/LinkPreview'; +import { isLinkSneaky, shouldLinkifyMessage } from '../../types/LinkPreview'; import { splitByEmoji } from '../../util/emoji'; import { missingCaseError } from '../../util/missingCaseError'; @@ -333,6 +333,10 @@ export class Linkify extends React.Component { | Array { const { text, renderNonLink } = this.props; + if (!shouldLinkifyMessage(text)) { + return text; + } + // We have to do this, because renderNonLink is not required in our Props object, // but it is always provided via defaultProps. if (!renderNonLink) { diff --git a/ts/test-node/types/LinkPreview_test.ts b/ts/test-node/types/LinkPreview_test.ts index d9a952270..45d7b7656 100644 --- a/ts/test-node/types/LinkPreview_test.ts +++ b/ts/test-node/types/LinkPreview_test.ts @@ -5,6 +5,7 @@ import { assert } from 'chai'; import { findLinks, + shouldLinkifyMessage, shouldPreviewHref, isLinkSneaky, } from '../../types/LinkPreview'; @@ -44,6 +45,26 @@ describe('Link previews', () => { }); }); + describe('#shouldLinkifyMessage;', () => { + it('returns false for strings with directional override characters', () => { + assert.isFalse(shouldLinkifyMessage('\u202c')); + assert.isFalse(shouldLinkifyMessage('\u202d')); + assert.isFalse(shouldLinkifyMessage('\u202e')); + }); + + it('returns false for strings with unicode drawing characters', () => { + assert.isFalse(shouldLinkifyMessage('\u2500')); + assert.isFalse(shouldLinkifyMessage('\u2588')); + assert.isFalse(shouldLinkifyMessage('\u25FF')); + }); + + it('returns true other strings', () => { + assert.isTrue(shouldLinkifyMessage(null)); + assert.isTrue(shouldLinkifyMessage(undefined)); + assert.isTrue(shouldLinkifyMessage('Random other string aqu%C3%AD')); + }); + }); + describe('#findLinks', () => { it('returns all links if no caretLocation is provided', () => { const text = diff --git a/ts/types/LinkPreview.ts b/ts/types/LinkPreview.ts index 6be7e8fef..7f9e9fc47 100644 --- a/ts/types/LinkPreview.ts +++ b/ts/types/LinkPreview.ts @@ -38,6 +38,26 @@ export function shouldPreviewHref(href: string): boolean { ); } +const DIRECTIONAL_OVERRIDES = /[\u202c\u202d\u202e]/; +const UNICODE_DRAWING = /[\u2500-\u25FF]/; + +export function shouldLinkifyMessage( + message: string | null | undefined +): boolean { + if (!message) { + return true; + } + + if (DIRECTIONAL_OVERRIDES.test(message)) { + return false; + } + if (UNICODE_DRAWING.test(message)) { + return false; + } + + return true; +} + export function isStickerPack(link = ''): boolean { return link.startsWith('https://signal.art/addstickers/'); } @@ -47,6 +67,10 @@ export function isGroupLink(link = ''): boolean { } export function findLinks(text: string, caretLocation?: number): Array { + if (!shouldLinkifyMessage(text)) { + return []; + } + const haveCaretLocation = isNumber(caretLocation); const textLength = text ? text.length : 0;