From 773aa9af195e2e70f16b3d2bf49b526071e40644 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 30 Jun 2021 10:00:02 -0700 Subject: [PATCH] Better emoji support in linkify/previews --- js/modules/link_previews.d.ts | 14 -- js/modules/link_previews.js | 163 ------------------ js/modules/signal.js | 2 - test/modules/link_previews_test.js | 11 +- ts/components/conversation/Emojify.tsx | 58 ++----- .../conversation/Linkify.stories.tsx | 8 + ts/components/conversation/Linkify.tsx | 57 +++--- ts/models/messages.ts | 5 +- ts/state/selectors/linkPreviews.ts | 8 +- ts/state/selectors/message.ts | 2 +- ts/test-both/util/emoji_test.ts | 53 ++++++ ts/types/LinkPreview.ts | 160 ++++++++++++++++- ts/util/emoji.ts | 36 ++++ ts/views/conversation_view.ts | 18 +- ts/window.d.ts | 2 - 15 files changed, 337 insertions(+), 260 deletions(-) delete mode 100644 js/modules/link_previews.d.ts delete mode 100644 js/modules/link_previews.js create mode 100644 ts/test-both/util/emoji_test.ts create mode 100644 ts/util/emoji.ts diff --git a/js/modules/link_previews.d.ts b/js/modules/link_previews.d.ts deleted file mode 100644 index 7c5c67a9f..000000000 --- a/js/modules/link_previews.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright 2019-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -export function isLinkSafeToPreview(link: string): boolean; - -export function findLinks(text: string, caretLocation?: number): Array; - -export function getDomain(href: string): string; - -export function isGroupLink(href: string): boolean; - -export function isLinkSneaky(link: string): boolean; - -export function isStickerPack(href: string): boolean; diff --git a/js/modules/link_previews.js b/js/modules/link_previews.js deleted file mode 100644 index ced80a812..000000000 --- a/js/modules/link_previews.js +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2019-2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -const { isNumber, compact, isEmpty, range } = require('lodash'); -const nodeUrl = require('url'); -const LinkifyIt = require('linkify-it'); -const { maybeParseUrl } = require('../../ts/util/url'); - -const linkify = LinkifyIt(); - -module.exports = { - findLinks, - getDomain, - isGroupLink, - isLinkSafeToPreview, - isLinkSneaky, - isStickerPack, -}; - -function isLinkSafeToPreview(href) { - const url = maybeParseUrl(href); - return Boolean(url && url.protocol === 'https:' && !isLinkSneaky(href)); -} - -function isStickerPack(link) { - return (link || '').startsWith('https://signal.art/addstickers/'); -} - -function isGroupLink(link) { - return (link || '').startsWith('https://signal.group/'); -} - -function findLinks(text, caretLocation) { - const haveCaretLocation = isNumber(caretLocation); - const textLength = text ? text.length : 0; - - const matches = linkify.match(text || '') || []; - return compact( - matches.map(match => { - if (!haveCaretLocation) { - return match.text; - } - - if (match.lastIndex === textLength && caretLocation === textLength) { - return match.text; - } - - if (match.index > caretLocation || match.lastIndex < caretLocation) { - return match.text; - } - - return null; - }) - ); -} - -function getDomain(href) { - const url = maybeParseUrl(href); - return url ? url.hostname : null; -} - -// See . -const VALID_URI_CHARACTERS = new Set([ - '%', - // "gen-delims" - ':', - '/', - '?', - '#', - '[', - ']', - '@', - // "sub-delims" - '!', - '$', - '&', - "'", - '(', - ')', - '*', - '+', - ',', - ';', - '=', - // unreserved - ...String.fromCharCode(...range(65, 91), ...range(97, 123)), - ...range(10).map(String), - '-', - '.', - '_', - '~', -]); -const ASCII_PATTERN = new RegExp('[\\u0020-\\u007F]', 'g'); -const MAX_HREF_LENGTH = 2 ** 12; - -function isLinkSneaky(href) { - // This helps users avoid extremely long links (which could be hiding something - // sketchy) and also sidesteps the performance implications of extremely long hrefs. - if (href.length > MAX_HREF_LENGTH) { - return true; - } - - const url = maybeParseUrl(href); - - // If we can't parse it, it's sneaky. - if (!url) { - return true; - } - - // Any links which contain auth are considered sneaky - if (url.username || url.password) { - return true; - } - - // If the domain is falsy, something fishy is going on - if (!url.hostname) { - return true; - } - - // To quote [RFC 1034][0]: "the total number of octets that represent a - // domain name [...] is limited to 255." To be extra careful, we set a - // maximum of 2048. (This also uses the string's `.length` property, - // which isn't exactly the same thing as the number of octets.) - // [0]: https://tools.ietf.org/html/rfc1034 - if (url.hostname.length > 2048) { - return true; - } - - // Domains cannot contain encoded characters - if (url.hostname.includes('%')) { - return true; - } - - // There must be at least 2 domain labels, and none of them can be empty. - const labels = url.hostname.split('.'); - if (labels.length < 2 || labels.some(isEmpty)) { - return true; - } - - // This is necesary because getDomain returns domains in punycode form. - const unicodeDomain = nodeUrl.domainToUnicode - ? nodeUrl.domainToUnicode(url.hostname) - : url.hostname; - - const withoutPeriods = unicodeDomain.replace(/\./g, ''); - - const hasASCII = ASCII_PATTERN.test(withoutPeriods); - const withoutASCII = withoutPeriods.replace(ASCII_PATTERN, ''); - - const isMixed = hasASCII && withoutASCII.length > 0; - if (isMixed) { - return true; - } - - // We can't use `url.pathname` (and so on) because it automatically encodes strings. - // For example, it turns `/aquí` into `/aqu%C3%AD`. - const startOfPathAndHash = href.indexOf('/', url.protocol.length + 4); - const pathAndHash = - startOfPathAndHash === -1 ? '' : href.substr(startOfPathAndHash); - return [...pathAndHash].some( - character => !VALID_URI_CHARACTERS.has(character) - ); -} diff --git a/js/modules/signal.js b/js/modules/signal.js index 59ef9be8e..2d385f568 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -21,7 +21,6 @@ const Stickers = require('./stickers'); const Settings = require('./settings'); const RemoteConfig = require('../../ts/RemoteConfig'); const Util = require('../../ts/util'); -const LinkPreviews = require('./link_previews'); // Components const { @@ -445,7 +444,6 @@ exports.setup = (options = {}) => { Groups, GroupChange, IndexedDB, - LinkPreviews, Migrations, Notifications, OS, diff --git a/test/modules/link_previews_test.js b/test/modules/link_previews_test.js index 738787b7a..c632ef2af 100644 --- a/test/modules/link_previews_test.js +++ b/test/modules/link_previews_test.js @@ -7,7 +7,7 @@ const { findLinks, isLinkSafeToPreview, isLinkSneaky, -} = require('../../js/modules/link_previews'); +} = require('../../ts/types/LinkPreview'); describe('Link previews', () => { describe('#isLinkSafeToPreview', () => { @@ -54,6 +54,15 @@ describe('Link previews', () => { assert.deepEqual(expected, actual); }); + it('returns all links after emojis without spaces in between', () => { + const text = '😎https://github.com/signalapp/Signal-Desktop😛'; + + const expected = ['https://github.com/signalapp/Signal-Desktop']; + + const actual = findLinks(text); + assert.deepEqual(expected, actual); + }); + it('includes all links if cursor is not in a link', () => { const text = 'Check out this link: https://github.com/signalapp/Signal-Desktop\nAnd this one too: https://github.com/signalapp/Signal-Android'; diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx index 255b9f5a1..3a18d1be0 100644 --- a/ts/components/conversation/Emojify.tsx +++ b/ts/components/conversation/Emojify.tsx @@ -5,9 +5,9 @@ import React from 'react'; import classNames from 'classnames'; -import emojiRegex from 'emoji-regex'; - import { RenderTextCallbackType } from '../../types/Util'; +import { splitByEmoji } from '../../util/emoji'; +import { missingCaseError } from '../../util/missingCaseError'; import { emojiToImage, SizeClassType } from '../emoji/lib'; // Some of this logic taken from emoji-js/replacement @@ -19,23 +19,23 @@ function getImageTag({ sizeClass, key, }: { - match: RegExpExecArray; + match: string; sizeClass?: SizeClassType; key: string | number; -}) { - const img = emojiToImage(match[0]); +}): JSX.Element | string { + const img = emojiToImage(match); if (!img) { - return match[0]; + return match; } return ( ); } @@ -53,15 +53,8 @@ export class Emojify extends React.Component { renderNonEmoji: ({ text }) => text, }; - public render(): - | JSX.Element - | string - | null - | Array { + public render(): null | Array { const { text, sizeClass, renderNonEmoji } = this.props; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const results: Array = []; - const regex = emojiRegex(); // We have to do this, because renderNonEmoji is not required in our Props object, // but it is always provided via defaultProps. @@ -69,33 +62,16 @@ export class Emojify extends React.Component { return null; } - let match = regex.exec(text); - let last = 0; - let count = 1; - - if (!match) { - return renderNonEmoji({ text, key: 0 }); - } - - while (match) { - if (last < match.index) { - const textWithNoEmoji = text.slice(last, match.index); - count += 1; - results.push(renderNonEmoji({ text: textWithNoEmoji, key: count })); + return splitByEmoji(text).map(({ type, value: match }, index) => { + if (type === 'emoji') { + return getImageTag({ match, sizeClass, key: index }); } - count += 1; - results.push(getImageTag({ match, sizeClass, key: count })); + if (type === 'text') { + return renderNonEmoji({ text: match, key: index }); + } - last = regex.lastIndex; - match = regex.exec(text); - } - - if (last < text.length) { - count += 1; - results.push(renderNonEmoji({ text: text.slice(last), key: count })); - } - - return results; + throw missingCaseError(type); + }); } } diff --git a/ts/components/conversation/Linkify.stories.tsx b/ts/components/conversation/Linkify.stories.tsx index 02dc81925..63a1efcd3 100644 --- a/ts/components/conversation/Linkify.stories.tsx +++ b/ts/components/conversation/Linkify.stories.tsx @@ -32,6 +32,14 @@ story.add('Links with Text', () => { return ; }); +story.add('Links with Emoji without space', () => { + const props = createProps({ + text: '👍https://www.signal.org😎', + }); + + return ; +}); + story.add('No Link', () => { const props = createProps({ text: 'I am fond of cats', diff --git a/ts/components/conversation/Linkify.tsx b/ts/components/conversation/Linkify.tsx index 78b4063ac..320a65fef 100644 --- a/ts/components/conversation/Linkify.tsx +++ b/ts/components/conversation/Linkify.tsx @@ -6,7 +6,9 @@ import React from 'react'; import LinkifyIt from 'linkify-it'; import { RenderTextCallbackType } from '../../types/Util'; -import { isLinkSneaky } from '../../../js/modules/link_previews'; +import { isLinkSneaky } from '../../types/LinkPreview'; +import { splitByEmoji } from '../../util/emoji'; +import { missingCaseError } from '../../util/missingCaseError'; const linkify = LinkifyIt() // This is all of the TLDs in place in 2010, according to [Wikipedia][0]. Note that @@ -55,10 +57,6 @@ export class Linkify extends React.Component { | null | Array { const { text, renderNonLink } = this.props; - const matchData = linkify.match(text) || []; - const results: Array = []; - let last = 0; - let count = 1; // We have to do this, because renderNonLink is not required in our Props object, // but it is always provided via defaultProps. @@ -66,19 +64,34 @@ export class Linkify extends React.Component { return null; } - if (matchData.length === 0) { - return renderNonLink({ text, key: 0 }); - } + const chunkData: Array<{ + chunk: string; + matchData: LinkifyIt.Match[]; + }> = splitByEmoji(text).map(({ type, value: chunk }) => { + if (type === 'text') { + return { chunk, matchData: linkify.match(chunk) || [] }; + } - matchData.forEach( - (match: { - index: number; - url: string; - lastIndex: number; - text: string; - }) => { + if (type === 'emoji') { + return { chunk, matchData: [] }; + } + + throw missingCaseError(type); + }); + + const results: Array = []; + let last = 0; + let count = 1; + + chunkData.forEach(({ chunk, matchData }) => { + if (matchData.length === 0) { + results.push(renderNonLink({ text: chunk, key: 0 })); + return; + } + + matchData.forEach(match => { if (last < match.index) { - const textWithNoLink = text.slice(last, match.index); + const textWithNoLink = chunk.slice(last, match.index); count += 1; results.push(renderNonLink({ text: textWithNoLink, key: count })); } @@ -96,13 +109,13 @@ export class Linkify extends React.Component { } last = match.lastIndex; - } - ); + }); - if (last < text.length) { - count += 1; - results.push(renderNonLink({ text: text.slice(last), key: count })); - } + if (last < chunk.length) { + count += 1; + results.push(renderNonLink({ text: chunk.slice(last), key: count })); + } + }); return results; } diff --git a/ts/models/messages.ts b/ts/models/messages.ts index aa5ac88f0..be93b0246 100644 --- a/ts/models/messages.ts +++ b/ts/models/messages.ts @@ -77,6 +77,7 @@ import { ReadReceipts } from '../messageModifiers/ReadReceipts'; import { ReadSyncs } from '../messageModifiers/ReadSyncs'; import { ViewSyncs } from '../messageModifiers/ViewSyncs'; import * as AttachmentDownloads from '../messageModifiers/AttachmentDownloads'; +import * as LinkPreview from '../types/LinkPreview'; /* eslint-disable camelcase */ /* eslint-disable more/no-then */ @@ -2644,13 +2645,13 @@ export class MessageModel extends window.Backbone.Model { try { const now = new Date().getTime(); - const urls = window.Signal.LinkPreviews.findLinks(dataMessage.body); + const urls = LinkPreview.findLinks(dataMessage.body); const incomingPreview = dataMessage.preview || []; const preview = incomingPreview.filter( (item: typeof window.WhatIsThis) => (item.image || item.title) && urls.includes(item.url) && - window.Signal.LinkPreviews.isLinkSafeToPreview(item.url) + LinkPreview.isLinkSafeToPreview(item.url) ); if (preview.length < incomingPreview.length) { window.log.info( diff --git a/ts/state/selectors/linkPreviews.ts b/ts/state/selectors/linkPreviews.ts index 10790acf8..001a60bd5 100644 --- a/ts/state/selectors/linkPreviews.ts +++ b/ts/state/selectors/linkPreviews.ts @@ -3,15 +3,21 @@ import { createSelector } from 'reselect'; +import { assert } from '../../util/assert'; +import { getDomain } from '../../types/LinkPreview'; + import { StateType } from '../reducer'; export const getLinkPreview = createSelector( ({ linkPreviews }: StateType) => linkPreviews.linkPreview, linkPreview => { if (linkPreview) { + const domain = getDomain(linkPreview.url); + assert(domain !== undefined, "Domain of linkPreview can't be undefined"); + return { ...linkPreview, - domain: window.Signal.LinkPreviews.getDomain(linkPreview.url), + domain, isLoaded: true, }; } diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index 144e11e23..b81f7c7d6 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -25,7 +25,7 @@ import { import { PropsType as ProfileChangeNotificationPropsType } from '../../components/conversation/ProfileChangeNotification'; import { QuotedAttachmentType } from '../../components/conversation/Quote'; -import { getDomain, isStickerPack } from '../../../js/modules/link_previews'; +import { getDomain, isStickerPack } from '../../types/LinkPreview'; import { ContactType, contactSelector } from '../../types/Contact'; import { BodyRangesType } from '../../types/Util'; diff --git a/ts/test-both/util/emoji_test.ts b/ts/test-both/util/emoji_test.ts new file mode 100644 index 000000000..0eb24968d --- /dev/null +++ b/ts/test-both/util/emoji_test.ts @@ -0,0 +1,53 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +import { assert } from 'chai'; + +import { replaceEmojiWithSpaces, splitByEmoji } from '../../util/emoji'; + +describe('emoji', () => { + describe('replaceEmojiWithSpaces', () => { + it('replaces emoji and pictograms with a single space', () => { + assert.strictEqual( + replaceEmojiWithSpaces('hello🌀🐀🔀😀world'), + 'hello world' + ); + }); + + it('leaves regular text as it is', () => { + assert.strictEqual( + replaceEmojiWithSpaces('Привет 嘿 հեյ העלא مرحبا '), + 'Привет 嘿 հեյ העלא مرحبا ' + ); + }); + }); + + describe('splitByEmoji', () => { + it('replaces emoji and pictograms with a single space', () => { + assert.deepStrictEqual(splitByEmoji('hello😛world😎😛!'), [ + { type: 'text', value: 'hello' }, + { type: 'emoji', value: '😛' }, + { type: 'text', value: 'world' }, + { type: 'emoji', value: '😎' }, + { type: 'text', value: '' }, + { type: 'emoji', value: '😛' }, + { type: 'text', value: '!' }, + ]); + }); + + it('should return empty string after split at the end', () => { + assert.deepStrictEqual(splitByEmoji('hello😛'), [ + { type: 'text', value: 'hello' }, + { type: 'emoji', value: '😛' }, + { type: 'text', value: '' }, + ]); + }); + + it('should return empty string before the split at the start', () => { + assert.deepStrictEqual(splitByEmoji('😛hello'), [ + { type: 'text', value: '' }, + { type: 'emoji', value: '😛' }, + { type: 'text', value: 'hello' }, + ]); + }); + }); +}); diff --git a/ts/types/LinkPreview.ts b/ts/types/LinkPreview.ts index 94253b9b3..607be82c8 100644 --- a/ts/types/LinkPreview.ts +++ b/ts/types/LinkPreview.ts @@ -1,6 +1,13 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2019-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { isNumber, compact, isEmpty, range } from 'lodash'; +import nodeUrl from 'url'; +import LinkifyIt from 'linkify-it'; + +import { maybeParseUrl } from '../util/url'; +import { replaceEmojiWithSpaces } from '../util/emoji'; + import { AttachmentType } from './Attachment'; export type LinkPreviewImage = AttachmentType & { @@ -18,3 +25,154 @@ export type LinkPreviewResult = { export type LinkPreviewWithDomain = { domain: string; } & LinkPreviewResult; + +const linkify = LinkifyIt(); + +export function isLinkSafeToPreview(href: string): boolean { + const url = maybeParseUrl(href); + return Boolean(url && url.protocol === 'https:' && !isLinkSneaky(href)); +} + +export function isStickerPack(link = ''): boolean { + return link.startsWith('https://signal.art/addstickers/'); +} + +export function isGroupLink(link = ''): boolean { + return link.startsWith('https://signal.group/'); +} + +export function findLinks(text: string, caretLocation?: number): Array { + const haveCaretLocation = isNumber(caretLocation); + const textLength = text ? text.length : 0; + + const matches = linkify.match(text ? replaceEmojiWithSpaces(text) : '') || []; + return compact( + matches.map(match => { + if (!haveCaretLocation) { + return match.text; + } + + if (caretLocation === undefined) { + return null; + } + + if (match.lastIndex === textLength && caretLocation === textLength) { + return match.text; + } + + if (match.index > caretLocation || match.lastIndex < caretLocation) { + return match.text; + } + + return null; + }) + ); +} + +export function getDomain(href: string): string | undefined { + const url = maybeParseUrl(href); + return url ? url.hostname : undefined; +} + +// See . +const VALID_URI_CHARACTERS = new Set([ + '%', + // "gen-delims" + ':', + '/', + '?', + '#', + '[', + ']', + '@', + // "sub-delims" + '!', + '$', + '&', + "'", + '(', + ')', + '*', + '+', + ',', + ';', + '=', + // unreserved + ...String.fromCharCode(...range(65, 91), ...range(97, 123)), + ...range(10).map(String), + '-', + '.', + '_', + '~', +]); +const ASCII_PATTERN = new RegExp('[\\u0020-\\u007F]', 'g'); +const MAX_HREF_LENGTH = 2 ** 12; + +export function isLinkSneaky(href: string): boolean { + // This helps users avoid extremely long links (which could be hiding something + // sketchy) and also sidesteps the performance implications of extremely long hrefs. + if (href.length > MAX_HREF_LENGTH) { + return true; + } + + const url = maybeParseUrl(href); + + // If we can't parse it, it's sneaky. + if (!url) { + return true; + } + + // Any links which contain auth are considered sneaky + if (url.username || url.password) { + return true; + } + + // If the domain is falsy, something fishy is going on + if (!url.hostname) { + return true; + } + + // To quote [RFC 1034][0]: "the total number of octets that represent a + // domain name [...] is limited to 255." To be extra careful, we set a + // maximum of 2048. (This also uses the string's `.length` property, + // which isn't exactly the same thing as the number of octets.) + // [0]: https://tools.ietf.org/html/rfc1034 + if (url.hostname.length > 2048) { + return true; + } + + // Domains cannot contain encoded characters + if (url.hostname.includes('%')) { + return true; + } + + // There must be at least 2 domain labels, and none of them can be empty. + const labels = url.hostname.split('.'); + if (labels.length < 2 || labels.some(isEmpty)) { + return true; + } + + // This is necesary because getDomain returns domains in punycode form. + const unicodeDomain = nodeUrl.domainToUnicode + ? nodeUrl.domainToUnicode(url.hostname) + : url.hostname; + + const withoutPeriods = unicodeDomain.replace(/\./g, ''); + + const hasASCII = ASCII_PATTERN.test(withoutPeriods); + const withoutASCII = withoutPeriods.replace(ASCII_PATTERN, ''); + + const isMixed = hasASCII && withoutASCII.length > 0; + if (isMixed) { + return true; + } + + // We can't use `url.pathname` (and so on) because it automatically encodes strings. + // For example, it turns `/aquí` into `/aqu%C3%AD`. + const startOfPathAndHash = href.indexOf('/', url.protocol.length + 4); + const pathAndHash = + startOfPathAndHash === -1 ? '' : href.substr(startOfPathAndHash); + return [...pathAndHash].some( + character => !VALID_URI_CHARACTERS.has(character) + ); +} diff --git a/ts/util/emoji.ts b/ts/util/emoji.ts new file mode 100644 index 000000000..45fabf711 --- /dev/null +++ b/ts/util/emoji.ts @@ -0,0 +1,36 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +/* eslint-disable no-restricted-syntax */ + +import emojiRegex from 'emoji-regex/es2015/RGI_Emoji'; + +import { assert } from './assert'; + +const REGEXP = emojiRegex(); + +export function replaceEmojiWithSpaces(value: string): string { + return value.replace(REGEXP, ' '); +} + +export type SplitElement = Readonly<{ + type: 'emoji' | 'text'; + value: string; +}>; + +export function splitByEmoji(value: string): ReadonlyArray { + const emojis = value.matchAll(REGEXP); + + const result: Array = []; + let lastIndex = 0; + for (const match of emojis) { + result.push({ type: 'text', value: value.slice(lastIndex, match.index) }); + result.push({ type: 'emoji', value: match[0] }); + + assert(match.index !== undefined, '`matchAll` should provide indices'); + lastIndex = match.index + match[0].length; + } + + result.push({ type: 'text', value: value.slice(lastIndex) }); + + return result; +} diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 53b126427..366b7db9e 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -47,6 +47,7 @@ import { LinkPreviewResult, LinkPreviewWithDomain, } from '../types/LinkPreview'; +import * as LinkPreview from '../types/LinkPreview'; type AttachmentOptions = { messageId: string; @@ -3982,7 +3983,7 @@ Whisper.ConversationView = Whisper.View.extend({ return; } - const links = window.Signal.LinkPreviews.findLinks(message, caretLocation); + const links = LinkPreview.findLinks(message, caretLocation); const { currentlyMatchedLink } = this; if (links.includes(currentlyMatchedLink)) { return; @@ -3993,7 +3994,7 @@ Whisper.ConversationView = Whisper.View.extend({ const link = links.find( item => - window.Signal.LinkPreviews.isLinkSafeToPreview(item) && + LinkPreview.isLinkSafeToPreview(item) && !this.excludedPreviewUrls.includes(item) ); if (!link) { @@ -4189,15 +4190,15 @@ Whisper.ConversationView = Whisper.View.extend({ url: string, abortSignal: Readonly ): Promise { - if (window.Signal.LinkPreviews.isStickerPack(url)) { + if (LinkPreview.isStickerPack(url)) { return this.getStickerPackPreview(url, abortSignal); } - if (window.Signal.LinkPreviews.isGroupLink(url)) { + if (LinkPreview.isGroupLink(url)) { return this.getGroupPreview(url, abortSignal); } // This is already checked elsewhere, but we want to be extra-careful. - if (!window.Signal.LinkPreviews.isLinkSafeToPreview(url)) { + if (!LinkPreview.isLinkSafeToPreview(url)) { return null; } @@ -4211,10 +4212,7 @@ Whisper.ConversationView = Whisper.View.extend({ const { title, imageHref, description, date } = linkPreviewMetadata; let image; - if ( - imageHref && - window.Signal.LinkPreviews.isLinkSafeToPreview(imageHref) - ) { + if (imageHref && LinkPreview.isLinkSafeToPreview(imageHref)) { let objectUrl: void | string; try { const fullSizeImage = await window.textsecure.messaging.fetchLinkPreviewImage( @@ -4406,7 +4404,7 @@ Whisper.ConversationView = Whisper.View.extend({ const [preview] = this.preview; return { ...preview, - domain: window.Signal.LinkPreviews.getDomain(preview.url), + domain: LinkPreview.getDomain(preview.url), }; }, diff --git a/ts/window.d.ts b/ts/window.d.ts index 954a87a5d..819573a88 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -10,7 +10,6 @@ import moment from 'moment'; import PQueue from 'p-queue/dist'; import { Ref } from 'react'; import { imageToBlurHash } from './util/imageToBlurHash'; -import * as LinkPreviews from '../js/modules/link_previews.d'; import * as Util from './util'; import { ConversationModelCollectionType, @@ -439,7 +438,6 @@ declare global { VisualAttachment: any; }; Util: typeof Util; - LinkPreviews: typeof LinkPreviews; GroupChange: { renderChange: (change: unknown, things: unknown) => Array; };