Process text story messages

This commit is contained in:
Josh Perez 2022-04-05 21:18:07 -04:00 committed by GitHub
parent 11d54f6769
commit fc9bdf9398
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 955 additions and 6 deletions

View File

@ -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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg fill="none" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="m11.6271 15.556 1.2775-1.2858c-1.0086-.0925-1.7145-.4119-2.2356-.9329-1.42036-1.4204-1.42036-3.43744-.0084-4.84938l2.7903-2.79027c1.4203-1.42035 3.429-1.42035 4.8493 0 1.4288 1.43716 1.4203 3.43741.0084 4.84935l-1.4456 1.4456c.269.6135.353 1.3615.2102 2.0086l2.412-2.4036c2.0675-2.05913 2.0759-4.98387-.0084-7.06817-2.0843-2.0927-5.009-2.07589-7.0681-.0168l-2.92474 2.93314c-2.05908 2.05908-2.07589 4.98383.00841 7.06813.49586.4874 1.15983.8572 2.13473 1.0421zm.7396-7.08489-1.2691 1.28588c1.0001.09244 1.7145.42021 2.2272.93291 1.4287 1.4287 1.4287 3.4374.0084 4.8493l-2.7903 2.7903c-1.42033 1.4203-3.42058 1.4203-4.84933 0-1.42875-1.4288-1.42035-3.4374 0-4.8494l1.43716-1.4455c-.26054-.6135-.35299-1.3531-.21012-2.0087l-2.40366 2.4037c-2.06749 2.0591-2.07589 4.9922.0084 7.0765 2.0843 2.0843 5.00904 2.0675 7.06815.0084l2.9163-2.9247c2.0675-2.0675 2.0759-4.9922-.0084-7.06814-.4875-.49586-1.1514-.86565-2.1347-1.05055z" fill="#000"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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;
}
}
}
}

View File

@ -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';

View File

@ -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 = (
<TextAttachment i18n={i18n} textAttachment={attachment.textAttachment} />
);
} else if (isNotReadyToShow) {
storyElement = (
<Blurhash
hash={attachment.blurHash || defaultBlurHash(ThemeType.dark)}

View File

@ -0,0 +1,245 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { storiesOf } from '@storybook/react';
import enMessages from '../../_locales/en/messages.json';
import { setupI18n } from '../util/setupI18n';
import { TextAttachment } from './TextAttachment';
import type { PropsType } from './TextAttachment';
const i18n = setupI18n('en', enMessages);
const getDefaultProps = (): PropsType => ({
i18n,
textAttachment: {},
});
const story = storiesOf('Components/TextAttachment', module);
story.add('Solid bg + text bg', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4286869806,
text: 'hello',
textBackgroundColor: 4293263387,
textForegroundColor: 4294967295,
textStyle: 1,
}}
/>
));
story.add('Gradient', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
gradient: {
angle: 191,
endColor: 4282529679,
startColor: 4294260804,
},
text: 'hey',
textBackgroundColor: 0,
textForegroundColor: 4294704123,
textStyle: 1,
}}
/>
));
story.add('Text with line breaks (condensed font)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
gradient: {
angle: 180,
endColor: 4278884698,
startColor: 4284861868,
},
text: 'Wow!\nThis is 2 lines!',
textBackgroundColor: 4294967295,
textForegroundColor: 4278249127,
textStyle: 5,
}}
/>
));
story.add('Text with line breaks + Autowrap (serif font)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4278249127,
text: 'Wrap?\nYes please, wrap this text automatically for me so that it fits nicely inside the story.',
textBackgroundColor: 0,
textForegroundColor: 4294967295,
textStyle: 3,
}}
/>
));
story.add('Autowrap text', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
gradient: {
angle: 175,
endColor: 4294859832,
startColor: 4294950980,
},
text: 'This text should automatically wrap into multiple lines since it exceeds the bounds of the story',
textBackgroundColor: 4294967295,
textForegroundColor: 4278249037,
textStyle: 1,
}}
/>
));
story.add('Romeo & Juliet', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
gradient: {
angle: 180,
endColor: 4286632135,
startColor: 4278227945,
},
text: "Two households, both alike in dignity, In fair Verona, where we lay our scene, From ancient grudge break to new mutiny, Where civil blood makes civil hands unclean. From forth the fatal loins of these two foes A pair of star-cross'd lovers take their life; Whose misadventured piteous overthrows Do with their death bury their parents' strife. The fearful passage of their death-mark'd love, And the continuance of their parents' rage, Which, but their children's end, nought could remove, Is now the two hours' traffic of our stage; The which if you with patient ears attend, What here shall miss, our toil shall strive to mend.",
textBackgroundColor: 0,
textForegroundColor: 4294704123,
textStyle: 4,
}}
/>
));
story.add('Overflow newline numbers', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
gradient: {
angle: 175,
endColor: 4294859832,
startColor: 4294950980,
},
text: '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16',
textBackgroundColor: 4294967295,
textForegroundColor: 4278249037,
textStyle: 1,
}}
/>
));
story.add('Character wrap (bold)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4278825851,
text: 'mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm',
textBackgroundColor: 0,
textForegroundColor: 4294704123,
textStyle: 2,
}}
/>
));
story.add('Mix of newlines, overflow, autowrap', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4294951251,
text: 'A new line\nIs this a new line? Yes, indeed and I should be wrapped woooooooooooooooooooooooow this is working!\nCool.',
textBackgroundColor: 0,
textForegroundColor: 4278231014,
textStyle: 1,
}}
/>
));
story.add('Link preview', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4294951251,
preview: {
url: 'https://www.signal.org/workworkwork',
title: 'Signal >> Careers',
// TODO add image
},
}}
/>
));
story.add('Link preview (long title)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4294951251,
preview: {
title:
'2021 Etihad Airways Abu Dhabi Grand Prix Race Summary - F1 RaceCast Dec 10 to Dec 12 - ESPN',
url: 'https://www.espn.com/f1/race/_/id/600001776',
},
text: 'Spoiler alert!',
textForegroundColor: 4294704123,
}}
/>
));
story.add('Link preview (just url)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4294951251,
preview: {
url: 'https://www.rolex.com/en-us/watches/day-date/m228236-0012.html',
},
}}
/>
));
story.add('Link preview (just url + text)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4294951251,
preview: {
url: 'https://www.rolex.com/en-us/watches/day-date/m228236-0012.html',
},
text: 'Check this out!',
}}
/>
));
story.add('Link preview (really long domain)', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
color: 4294951251,
preview: {
url: 'https://llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.international/',
},
}}
/>
));
story.add('Link Preview w/ R&J', () => (
<TextAttachment
{...getDefaultProps()}
textAttachment={{
gradient: {
angle: 180,
endColor: 4286632135,
startColor: 4278227945,
},
text: "Two households, both alike in dignity, In fair Verona, where we lay our scene, From ancient grudge break to new mutiny, Where civil blood makes civil hands unclean. From forth the fatal loins of these two foes A pair of star-cross'd lovers take their life; Whose misadventured piteous overthrows Do with their death bury their parents' strife. The fearful passage of their death-mark'd love, And the continuance of their parents' rage, Which, but their children's end, nought could remove, Is now the two hours' traffic of our stage; The which if you with patient ears attend, What here shall miss, our toil shall strive to mend.",
textBackgroundColor: 0,
textForegroundColor: 4294704123,
textStyle: 4,
preview: {
title: 'Romeo and Juliet: Entire Play',
url: 'http://shakespeare.mit.edu/romeo_juliet/full.html',
},
}}
/>
));

View File

@ -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 <AddNewLines key={key} text={textWithNewLines} />;
};
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<HTMLDivElement | null>(null);
const [linkPreviewOffsetTop, setLinkPreviewOffsetTop] = useState<
number | undefined
>();
return (
<Measure bounds>
{({ contentRect, measureRef }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
<div
className="TextAttachment"
onClick={() => {
if (linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
}}
onKeyUp={ev => {
if (ev.key === 'Escape' && linkPreviewOffsetTop) {
setLinkPreviewOffsetTop(undefined);
}
}}
ref={measureRef}
>
<div
className="TextAttachment__story"
style={{
background: getBackground(textAttachment),
transform: `scale(${(contentRect.bounds?.height || 1) / 1280})`,
}}
>
{textAttachment.text && (
<div
className="TextAttachment__text"
style={{
backgroundColor: textAttachment.textBackgroundColor
? getHexFromNumber(textAttachment.textBackgroundColor)
: 'none',
color: getHexFromNumber(
textAttachment.textForegroundColor || COLOR_WHITE_INT
),
font: getFont(
textAttachment.text,
getTextSize(textAttachment.text),
textAttachment.textStyle,
i18n
),
textAlign:
getTextSize(textAttachment.text) === TextSize.Small
? 'left'
: 'center',
}}
>
<div className="TextAttachment__text__container">
<Emojify
text={textAttachment.text}
renderNonEmoji={renderNewLines}
/>
</div>
</div>
)}
{textAttachment.preview && (
<>
{linkPreviewOffsetTop && textAttachment.preview.url && (
<a
className="TextAttachment__preview__tooltip"
href={textAttachment.preview.url}
rel="noreferrer"
style={{
top: linkPreviewOffsetTop - 150,
}}
target="_blank"
>
<div>
<div>{i18n('TextAttachment__preview__link')}</div>
<div className="TextAttachment__preview__tooltip__url">
{textAttachment.preview.url}
</div>
</div>
<div className="TextAttachment__preview__tooltip__arrow" />
</a>
)}
<div
className={classNames('TextAttachment__preview', {
'TextAttachment__preview--large': Boolean(
textAttachment.preview.title
),
})}
ref={linkPreview}
onFocus={() =>
setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop)
}
onMouseOver={() =>
setLinkPreviewOffsetTop(linkPreview?.current?.offsetTop)
}
>
<div className="TextAttachment__preview__image" />
<div className="TextAttachment__preview__title">
{textAttachment.preview.title && (
<div className="TextAttachment__preview__title__container">
{textAttachment.preview.title}
</div>
)}
<div className="TextAttachment__preview__url">
{getDomain(String(textAttachment.preview.url))}
</div>
</div>
</div>
</>
)}
</div>
</div>
)}
</Measure>
);
};

View File

@ -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);
});
});

View File

@ -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(

View File

@ -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 = {

View File

@ -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;
};

View File

@ -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 */

View File

@ -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<string> = [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(', ');
}

View File

@ -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<HTMLDivElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-04-06T00:59:17.194Z"
},
{
"rule": "React-useRef",
"path": "ts/components/Tooltip.tsx",