diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 62f1faad7..9838748be 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -7024,6 +7024,10 @@ "message": "Error displaying image", "description": "aria-label for image errors" }, + "TextAttachment__preview__link": { + "message": "Visit link", + "description": "Title for the link preview tooltip" + }, "WhatsNew__modal-title": { "message": "What's New", "description": "Title for the whats new modal" diff --git a/fonts/stories/BarlowCondensed-Medium.ttf b/fonts/stories/BarlowCondensed-Medium.ttf new file mode 100644 index 000000000..82e45ac06 Binary files /dev/null and b/fonts/stories/BarlowCondensed-Medium.ttf differ diff --git a/fonts/stories/EBGaramond-Regular.ttf b/fonts/stories/EBGaramond-Regular.ttf new file mode 100644 index 000000000..80a857b47 Binary files /dev/null and b/fonts/stories/EBGaramond-Regular.ttf differ diff --git a/fonts/stories/Parisienne-Regular.ttf b/fonts/stories/Parisienne-Regular.ttf new file mode 100644 index 000000000..720661369 Binary files /dev/null and b/fonts/stories/Parisienne-Regular.ttf differ diff --git a/images/icons/v2/link-24.svg b/images/icons/v2/link-24.svg new file mode 100644 index 000000000..ec0e9bcbd --- /dev/null +++ b/images/icons/v2/link-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stylesheets/components/TextAttachment.scss b/stylesheets/components/TextAttachment.scss new file mode 100644 index 000000000..8123ae16d --- /dev/null +++ b/stylesheets/components/TextAttachment.scss @@ -0,0 +1,153 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.TextAttachment { + max-height: 100%; + + &__story { + align-items: center; + border-radius: 12px; + display: flex; + flex-direction: column; + height: 1280px; + justify-content: center; + overflow: hidden; + transform-origin: top center; + user-select: none; + width: 720px; + } + + &__text { + border-radius: 36px; + padding: 28px; + margin-left: 72px; + margin-right: 72px; + + &__container { + -webkit-box-orient: vertical; + -webkit-line-clamp: 13; + display: -webkit-box; + overflow: hidden; + } + } + + &__preview { + align-items: center; + background: $color-black-alpha-40; + border-radius: 28px; + display: flex; + flex-direction: row; + height: 122px; + justify-content: center; + margin-left: 72px; + margin-right: 72px; + padding: 34px; + + &--large { + height: 192px; + } + + &__image { + align-items: center; + background-color: $color-white; + border-radius: 14px; + display: flex; + flex-direction: row; + height: 74px; + justify-content: center; + margin-right: 32px; + width: 74px; + + .TextAttachment__preview--large & { + height: 144px; + width: 144px; + } + + &::after { + @include color-svg('../images/icons/v2/link-24.svg', $color-black); + content: ''; + height: 44px; + width: 44px; + } + } + + &__title { + align-items: flex-start; + display: flex; + flex-direction: column; + justify-content: flex-start; + max-width: 422px; + + .TextAttachment__preview--large & { + max-width: 352px; + } + + &__container { + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; + display: -webkit-box; + font: bold 30px Inter; + overflow: hidden; + } + } + + &__url { + color: $color-white; + font: bold 30px Inter; + max-width: 422px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .TextAttachment__preview--large & { + color: $color-white-alpha-60; + font: 24px Inter; + max-width: 352px; + } + } + + &__tooltip { + align-items: center; + background: $color-black-alpha-90; + border-radius: 12px; + color: $color-white; + display: flex; + font-size: 30px; + justify-content: center; + line-height: 32px; + max-width: 656px; + padding: 24px 32px; + position: absolute; + text-decoration: none; + z-index: $z-index-above-base; + + &::after { + border-color: black transparent transparent transparent; + border-style: solid; + border-width: 14px; + content: ''; + left: 50%; + margin-left: -14px; + position: absolute; + top: 100%; + } + + &__url { + margin-top: 4px; + max-width: 566px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__arrow { + @include color-svg( + '../images/icons/v2/chevron-right-24.svg', + $color-white + ); + height: 24px; + width: 24px; + } + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index fd51d1b42..9f04d2440 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -107,6 +107,7 @@ @import './components/StoryViewer.scss'; @import './components/SystemMessage.scss'; @import './components/Tabs.scss'; +@import './components/TextAttachment.scss'; @import './components/TimelineDateHeader.scss'; @import './components/TimelineFloatingHeader.scss'; @import './components/TimelineWarning.scss'; diff --git a/ts/components/StoryImage.tsx b/ts/components/StoryImage.tsx index 296dd7b92..c02825cd8 100644 --- a/ts/components/StoryImage.tsx +++ b/ts/components/StoryImage.tsx @@ -8,6 +8,7 @@ import { Blurhash } from 'react-blurhash'; import type { AttachmentType } from '../types/Attachment'; import type { LocalizerType } from '../types/Util'; import { Spinner } from './Spinner'; +import { TextAttachment } from './TextAttachment'; import { ThemeType } from '../types/Util'; import { defaultBlurHash, @@ -57,7 +58,11 @@ export const StoryImage = ({ const getClassName = getClassNamesFor('StoryImage', moduleClassName); let storyElement: JSX.Element; - if (isNotReadyToShow) { + if (attachment.textAttachment) { + storyElement = ( + + ); + } else if (isNotReadyToShow) { storyElement = ( ({ + i18n, + textAttachment: {}, +}); + +const story = storiesOf('Components/TextAttachment', module); + +story.add('Solid bg + text bg', () => ( + +)); + +story.add('Gradient', () => ( + +)); + +story.add('Text with line breaks (condensed font)', () => ( + +)); + +story.add('Text with line breaks + Autowrap (serif font)', () => ( + +)); + +story.add('Autowrap text', () => ( + +)); + +story.add('Romeo & Juliet', () => ( + +)); + +story.add('Overflow newline numbers', () => ( + +)); + +story.add('Character wrap (bold)', () => ( + +)); + +story.add('Mix of newlines, overflow, autowrap', () => ( + +)); + +story.add('Link preview', () => ( + > Careers', + // TODO add image + }, + }} + /> +)); + +story.add('Link preview (long title)', () => ( + +)); + +story.add('Link preview (just url)', () => ( + +)); + +story.add('Link preview (just url + text)', () => ( + +)); + +story.add('Link preview (really long domain)', () => ( + +)); + +story.add('Link Preview w/ R&J', () => ( + +)); diff --git a/ts/components/TextAttachment.tsx b/ts/components/TextAttachment.tsx new file mode 100644 index 000000000..281e06506 --- /dev/null +++ b/ts/components/TextAttachment.tsx @@ -0,0 +1,214 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import Measure from 'react-measure'; +import React, { useRef, useState } from 'react'; +import classNames from 'classnames'; + +import type { LocalizerType, RenderTextCallbackType } from '../types/Util'; +import type { TextAttachmentType } from '../types/Attachment'; +import { AddNewLines } from './conversation/AddNewLines'; +import { Emojify } from './conversation/Emojify'; +import { TextAttachmentStyleType } from '../types/Attachment'; +import { count } from '../util/grapheme'; +import { getDomain } from '../types/LinkPreview'; +import { getFontNameByTextScript } from '../util/getFontNameByTextScript'; + +const renderNewLines: RenderTextCallbackType = ({ + text: textWithNewLines, + key, +}) => { + return ; +}; + +const CHAR_LIMIT_TEXT_LARGE = 50; +const CHAR_LIMIT_TEXT_MEDIUM = 200; +const COLOR_WHITE_INT = 4294704123; +const FONT_SIZE_LARGE = 64; +const FONT_SIZE_MEDIUM = 42; +const FONT_SIZE_SMALL = 32; + +enum TextSize { + Small, + Medium, + Large, +} + +export type PropsType = { + i18n: LocalizerType; + textAttachment: TextAttachmentType; +}; + +function getTextSize(text: string): TextSize { + const length = count(text); + + if (length < CHAR_LIMIT_TEXT_LARGE) { + return TextSize.Large; + } + + if (length < CHAR_LIMIT_TEXT_MEDIUM) { + return TextSize.Medium; + } + + return TextSize.Small; +} + +function getHexFromNumber(color: number): string { + return `#${color.toString(16).slice(2)}`; +} + +function getBackground({ color, gradient }: TextAttachmentType): string { + if (gradient) { + return `linear-gradient(${gradient.angle}deg, ${getHexFromNumber( + gradient.startColor || COLOR_WHITE_INT + )}, ${getHexFromNumber(gradient.endColor || COLOR_WHITE_INT)})`; + } + + return getHexFromNumber(color || COLOR_WHITE_INT); +} + +function getFont( + text: string, + textSize: TextSize, + textStyle?: TextAttachmentStyleType | null, + i18n?: LocalizerType +): string { + const textStyleIndex = Number(textStyle) || 0; + const fontName = getFontNameByTextScript(text, textStyleIndex, i18n); + + let fontSize = FONT_SIZE_SMALL; + switch (textSize) { + case TextSize.Large: + fontSize = FONT_SIZE_LARGE; + break; + case TextSize.Medium: + fontSize = FONT_SIZE_MEDIUM; + break; + default: + fontSize = FONT_SIZE_SMALL; + } + + const fontWeight = textStyle === TextAttachmentStyleType.BOLD ? 'bold ' : ''; + + return `${fontWeight}${fontSize}pt ${fontName}`; +} + +export const TextAttachment = ({ + i18n, + textAttachment, +}: PropsType): JSX.Element | null => { + const linkPreview = useRef(null); + const [linkPreviewOffsetTop, setLinkPreviewOffsetTop] = useState< + number | undefined + >(); + + return ( + + {({ contentRect, measureRef }) => ( + // eslint-disable-next-line jsx-a11y/no-static-element-interactions +
{ + if (linkPreviewOffsetTop) { + setLinkPreviewOffsetTop(undefined); + } + }} + onKeyUp={ev => { + if (ev.key === 'Escape' && linkPreviewOffsetTop) { + setLinkPreviewOffsetTop(undefined); + } + }} + ref={measureRef} + > +
+ {textAttachment.text && ( +
+
+ +
+
+ )} + {textAttachment.preview && ( + <> + {linkPreviewOffsetTop && textAttachment.preview.url && ( + +
+
{i18n('TextAttachment__preview__link')}
+
+ {textAttachment.preview.url} +
+
+
+ + )} +
+ setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop) + } + onMouseOver={() => + setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop) + } + > +
+
+ {textAttachment.preview.title && ( +
+ {textAttachment.preview.title} +
+ )} +
+ {getDomain(String(textAttachment.preview.url))} +
+
+
+ + )} +
+
+ )} + + ); +}; diff --git a/ts/test-both/util/getFontNameByTextScript_test.ts b/ts/test-both/util/getFontNameByTextScript_test.ts new file mode 100644 index 000000000..95ffebf6a --- /dev/null +++ b/ts/test-both/util/getFontNameByTextScript_test.ts @@ -0,0 +1,107 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { assert } from 'chai'; + +import { + fontSniffer, + getFontNameByTextScript, +} from '../../util/getFontNameByTextScript'; +import { setupI18n } from '../../util/setupI18n'; + +describe('getFontNameByTextScript', () => { + it('has arabic', () => { + const text = 'الثعلب البني السريع يقفز فوق الكلب الكسول'; + assert.isTrue(fontSniffer.hasArabic(text), 'arabic'); + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isFalse(fontSniffer.hasJapanese(text), 'japanese'); + }); + + it('has chinese (simplified)', () => { + const text = '敏捷的棕色狐狸跳过了懒狗'; + assert.isTrue(fontSniffer.hasCJK(text), 'cjk'); + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isFalse(fontSniffer.hasJapanese(text), 'japanese'); + }); + + it('has chinese (traditional)', () => { + const text = '敏捷的棕色狐狸跳過了懶狗'; + assert.isTrue(fontSniffer.hasCJK(text), 'cjk'); + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isFalse(fontSniffer.hasJapanese(text), 'japanese'); + }); + + it('has cyrillic (Bulgarian)', () => { + const text = 'Бързата кафява лисица прескача мързеливото куче'; + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isTrue(fontSniffer.hasCyrillic(text), 'cyrillic'); + assert.isFalse(fontSniffer.hasArabic(text), 'arabic'); + }); + + it('has cyrillic (Ukranian)', () => { + const text = 'Швидка бура лисиця стрибає через ледачого пса'; + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isTrue(fontSniffer.hasCyrillic(text), 'cyrillic'); + assert.isFalse(fontSniffer.hasArabic(text), 'arabic'); + }); + + it('has devanagari', () => { + const text = 'तेज, भूरी लोमडी आलसी कुत्ते के उपर कूद गई'; + assert.isTrue(fontSniffer.hasDevanagari(text), 'devanagari'); + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isFalse(fontSniffer.hasCyrillic(text), 'cyrillic'); + }); + + it('has japanese', () => { + const text = '速い茶色のキツネは怠惰な犬を飛び越えます'; + assert.isFalse(fontSniffer.hasDevanagari(text), 'devanagari'); + assert.isFalse(fontSniffer.hasLatin(text), 'latin'); + assert.isTrue(fontSniffer.hasJapanese(text), 'japanese'); + assert.isTrue(fontSniffer.hasCJK(text), 'cjk'); + }); + + it('throws when passing in an invalid text style', () => { + const text = 'abc'; + + assert.throws(() => { + getFontNameByTextScript(text, -1); + }); + + assert.throws(() => { + getFontNameByTextScript(text, 99); + }); + }); + + it('returns the correct font names in the right order (japanese)', () => { + const text = '速い茶色のキツネは怠惰な犬を飛び越えます'; + + const actual = getFontNameByTextScript(text, 0); + const expected = + '"Hiragino Sans W3", "PingFang SC Regular", SimHei, sans-serif'; + assert.equal(actual, expected); + }); + + it('returns the correct font names in the right order (latin)', () => { + const text = 'The quick brown fox jumps over the lazy dog'; + + const actual = getFontNameByTextScript(text, 0); + const expected = 'Inter, sans-serif'; + assert.equal(actual, expected); + }); + + it('returns the correct font names (chinese simplified)', () => { + const text = '敏捷的棕色狐狸跳过了懒狗'; + + const actual = getFontNameByTextScript(text, 0, setupI18n('zh_CN', {})); + const expected = '"PingFang SC Regular", SimHei, sans-serif'; + assert.equal(actual, expected); + }); + + it('returns the correct font names (chinese traditional)', () => { + const text = '敏捷的棕色狐狸跳過了懶狗'; + + const actual = getFontNameByTextScript(text, 0, setupI18n('zh_TW', {})); + const expected = '"PingFang TC Regular", "JhengHei TC Regular", sans-serif'; + assert.equal(actual, expected); + }); +}); diff --git a/ts/textsecure/MessageReceiver.ts b/ts/textsecure/MessageReceiver.ts index de7bf91a8..41509afb7 100644 --- a/ts/textsecure/MessageReceiver.ts +++ b/ts/textsecure/MessageReceiver.ts @@ -110,7 +110,9 @@ import { } from './messageReceiverEvents'; import * as log from '../logging/log'; import * as durations from '../util/durations'; +import { IMAGE_JPEG } from '../types/MIME'; import { areArraysMatchingSets } from '../util/areArraysMatchingSets'; +import { generateBlurHash } from '../util/generateBlurHash'; const GROUPV1_ID_LENGTH = 16; const GROUPV2_ID_LENGTH = 32; @@ -1800,11 +1802,16 @@ export default class MessageReceiver } if (msg.textAttachment) { - log.error( - 'MessageReceiver.handleStoryMessage: Got a textAttachment, cannot handle it', - logId - ); - return; + attachments.push({ + contentType: IMAGE_JPEG, + size: 0, + textAttachment: msg.textAttachment, + blurHash: generateBlurHash( + (msg.textAttachment.color || + msg.textAttachment.gradient?.startColor) ?? + undefined + ), + }); } const expireTimer = Math.min( diff --git a/ts/textsecure/Types.d.ts b/ts/textsecure/Types.d.ts index 1bc08d4ed..620b58dd0 100644 --- a/ts/textsecure/Types.d.ts +++ b/ts/textsecure/Types.d.ts @@ -4,6 +4,7 @@ import type { SignalService as Proto } from '../protobuf'; import type { IncomingWebSocketRequest } from './WebsocketResources'; import type { UUID } from '../types/UUID'; +import type { TextAttachmentType } from '../types/Attachment'; export { IdentityKeyType, @@ -105,6 +106,7 @@ export type ProcessedAttachment = { caption?: string; blurHash?: string; cdnNumber?: number; + textAttachment?: TextAttachmentType; }; export type ProcessedGroupContext = { diff --git a/ts/types/Attachment.ts b/ts/types/Attachment.ts index 6595a8923..4293300a8 100644 --- a/ts/types/Attachment.ts +++ b/ts/types/Attachment.ts @@ -66,11 +66,38 @@ export type AttachmentType = { cdnId?: string; cdnKey?: string; data?: Uint8Array; + textAttachment?: TextAttachmentType; /** Legacy field. Used only for downloading old attachments */ id?: number; }; +export enum TextAttachmentStyleType { + DEFAULT = 0, + REGULAR = 1, + BOLD = 2, + SERIF = 3, + SCRIPT = 4, + CONDENSED = 5, +} + +export type TextAttachmentType = { + text?: string | null; + textStyle?: number | null; + textForegroundColor?: number | null; + textBackgroundColor?: number | null; + preview?: { + url?: string | null; + title?: string | null; + } | null; + gradient?: { + startColor?: number | null; + endColor?: number | null; + angle?: number | null; + } | null; + color?: number | null; +}; + export type DownloadedAttachmentType = AttachmentType & { data: Uint8Array; }; diff --git a/ts/util/generateBlurHash.ts b/ts/util/generateBlurHash.ts new file mode 100644 index 000000000..0a2e4d11d --- /dev/null +++ b/ts/util/generateBlurHash.ts @@ -0,0 +1,16 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { encode83 } from 'blurhash/dist/base83'; + +/* eslint-disable no-bitwise */ +export function generateBlurHash(argb = 4294704123): string { + const R = 0xff & (argb >> 16); + const G = 0xff & (argb >> 8); + const B = 0xff & (argb >> 0); + + const value = (R << 16) + (G << 8) + B; + + return `00${encode83(value, 4)}`; +} +/* eslint-enable no-bitwise */ diff --git a/ts/util/getFontNameByTextScript.ts b/ts/util/getFontNameByTextScript.ts new file mode 100644 index 000000000..d713e6978 --- /dev/null +++ b/ts/util/getFontNameByTextScript.ts @@ -0,0 +1,160 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { LocalizerType } from '../types/Util'; +import { strictAssert } from './assert'; + +const FONT_MAP = { + base: [ + 'sans-serif', + 'sans-serif', + 'sans-serif', + 'serif', + 'serif', + 'sans-serif', + ], + latin: [ + 'Inter', + 'Inter', + 'Inter', + '"EB Garamond"', + 'Parisienne', + '"Barlow Condensed"', + ], + cyrillic: [ + 'Inter', + 'Inter', + 'Inter', + '"EB Garamond"', + '"American Typewriter Semibold", "Cambria Bold"', + '"SF Pro Light (System Light)", "Calibri Light"', + ], + devanagari: [ + '"Kohinoor Devanagari Regular", "Utsaah Regular"', + '"Kohinoor Devanagari Regular", "Utsaah Regular"', + '"Kohinoor Devanagari Semibold", "Utsaah Bold"', + '"Devanagari Sangam MN Regular", "Kokila Regular"', + '"Devanagari Sangam MN Bold", "Kokila Bold"', + '"Kohinoor Devanagari Light", "Utsaah Regular"', + ], + arabic: [ + '"SF Arabic Regular", "Segoe UI Arabic Regular"', + '"SF Arabic Regular", "Segoe UI Arabic Regular"', + '"SF Arabic Bold", "Segoe UI Arabic Bold"', + '"Geeza Pro Regular", "Sakkal Majalla Regular"', + '"Geeza Pro Bold", "Sakkal Majalla Bold"', + '"SF Arabic Black", "Segoe UI Arabic Bold"', + ], + japanese: [ + '"Hiragino Sans W3"', + '"Hiragino Sans W3"', + '"Hiragino Sans W7"', + '"Hiragino Mincho Pro W3"', + '"Hiragino Mincho Pro W6"', + '"Hiragino Maru Gothic Pro N"', + ], + zhhk: [ + '"PingFang HK Regular", "MingLiU Regular"', + '"PingFang HK Regular", "MingLiU Regular"', + '"PingFang HK Semibold", "MingLiU Regular"', + '"PingFang HK Ultralight", "MingLiU Regular"', + '"PingFang HK Thin", "MingLiU Regular"', + '"PingFang HK Light", "MingLiU Regular"', + ], + zhtc: [ + '"PingFang TC Regular", "JhengHei TC Regular"', + '"PingFang TC Regular", "JhengHei TC Regular"', + '"PingFang TC Semibold", "JhengHei TC Bold"', + '"PingFang TC Ultralight", "JhengHei TC Light"', + '"PingFang TC Thin", "JhengHei TC Regular"', + '"PingFang TC Light", "JhengHei TC Bold"', + ], + zhsc: [ + '"PingFang SC Regular", SimHei', + '"PingFang SC Regular", SimHei', + '"PingFang SC Semibold", SimHei', + '"PingFang SC Ultralight", SimHei', + '"PingFang SC Thin", SimHei', + '"PingFang SC Light", SimHei', + ], +}; + +const rxArabic = /\p{Script=Arab}/u; +const rxCJK = /\p{Script=Han}/u; +const rxCyrillic = /\p{Script=Cyrl}/u; +const rxDevanagari = /\p{Script=Deva}/u; +const rxJapanese = /\p{Script=Hira}|\p{Script=Kana}/u; +const rxLatin = /\p{Script=Latn}/u; + +export const fontSniffer = { + hasArabic(text: string): boolean { + return rxArabic.test(text); + }, + + hasCJK(text: string): boolean { + return rxCJK.test(text); + }, + + hasCyrillic(text: string): boolean { + return rxCyrillic.test(text); + }, + + hasDevanagari(text: string): boolean { + return rxDevanagari.test(text); + }, + + hasJapanese(text: string): boolean { + return rxJapanese.test(text); + }, + + hasLatin(text: string): boolean { + return rxLatin.test(text); + }, +}; + +export function getFontNameByTextScript( + text: string, + textStyleIndex: number, + i18n?: LocalizerType +): string { + strictAssert( + textStyleIndex >= 0 && textStyleIndex <= 5, + 'text style is not between 0-5' + ); + + const fonts: Array = [FONT_MAP.base[textStyleIndex]]; + + if (fontSniffer.hasArabic(text)) { + fonts.push(FONT_MAP.arabic[textStyleIndex]); + } + + if (fontSniffer.hasCJK(text)) { + const locale = i18n?.getLocale(); + + if (locale === 'zh_TW') { + fonts.push(FONT_MAP.zhtc[textStyleIndex]); + } else if (locale === 'zh_HK') { + fonts.push(FONT_MAP.zhhk[textStyleIndex]); + } else { + fonts.push(FONT_MAP.zhsc[textStyleIndex]); + } + } + + if (fontSniffer.hasCyrillic(text)) { + fonts.push(FONT_MAP.cyrillic[textStyleIndex]); + } + + if (fontSniffer.hasDevanagari(text)) { + fonts.push(FONT_MAP.devanagari[textStyleIndex]); + } + + if (fontSniffer.hasJapanese(text)) { + fonts.push(FONT_MAP.japanese[textStyleIndex]); + } + + if (fontSniffer.hasLatin(text)) { + fonts.push(FONT_MAP.latin[textStyleIndex]); + } + + return fonts.reverse().join(', '); +} diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 23dd6aa36..0549221b7 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -7700,6 +7700,13 @@ "reasonCategory": "usageTrusted", "updated": "2022-02-15T17:57:06.507Z" }, + { + "rule": "React-useRef", + "path": "ts/components/TextAttachment.tsx", + "line": " const linkPreview = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2022-04-06T00:59:17.194Z" + }, { "rule": "React-useRef", "path": "ts/components/Tooltip.tsx",