GIF attachments

This commit is contained in:
Fedor Indutny 2021-04-27 15:11:59 -07:00 committed by Scott Nonnenberg
parent 5f17d01f49
commit caf1d4c4da
15 changed files with 526 additions and 93 deletions

BIN
fixtures/cat-gif.mp4 Normal file

Binary file not shown.

BIN
fixtures/cat-screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -215,6 +215,7 @@ exports.deleteData = deleteOnDisk => {
exports.isImage = AttachmentTS.isImage;
exports.isVideo = AttachmentTS.isVideo;
exports.isGIF = AttachmentTS.isGIF;
exports.isAudio = AttachmentTS.isAudio;
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
exports.getUploadSizeLimitKb = AttachmentTS.getUploadSizeLimitKb;

View File

@ -42,6 +42,8 @@ const {
systemPreferences,
} = electron;
const animationSettings = systemPreferences.getAnimationSettings();
const appUserModelId = `org.whispersystems.${packageJson.name}`;
console.log('Set Windows Application User Model ID (AUMID)', {
appUserModelId,
@ -245,6 +247,9 @@ function prepareURL(pathSegments, moreKeys) {
contentProxyUrl: config.contentProxyUrl,
sfuUrl: config.get('sfuUrl'),
importMode: importMode ? true : undefined, // for stringify()
reducedMotionSetting: animationSettings.prefersReducedMotion
? true
: undefined,
serverPublicParams: config.get('serverPublicParams'),
serverTrustRoot: config.get('serverTrustRoot'),
appStartInitialSpellcheckSetting,

View File

@ -476,6 +476,10 @@ try {
activeWindowService
);
window.Accessibility = {
reducedMotionSetting: Boolean(config.reducedMotionSetting),
};
window.isValidGuid = maybeGuid =>
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(
maybeGuid

View File

@ -663,6 +663,10 @@
border-top-left-radius: 0px;
border-top-right-radius: 0px;
}
.module-message__container--gif & {
border-radius: inherit;
}
}
.module-message__sticker-container {
@ -4188,42 +4192,30 @@ button.module-conversation-details__action-button {
overflow: hidden;
}
.module-image--tap-to-play,
.module-image--not-downloaded {
align-items: center;
display: flex;
justify-content: center;
i {
span {
align-items: center;
display: flex;
justify-content: center;
border-radius: 48px;
height: 48px;
width: 48px;
@include light-theme {
background-color: $color-gray-65;
}
@include dark-theme {
background-color: $color-gray-75;
}
&:after {
content: '';
height: 17px;
width: 17px;
@include color-svg('../images/icons/v2/arrow-down-24.svg', $color-white);
}
background-color: $color-black-alpha-70;
}
&:hover {
i {
background-color: $color-black;
span {
background-color: $color-black-alpha-80;
}
}
&:focus {
i {
span {
background-color: $color-gray-75;
border: 4px solid $ultramarine-ui-light;
box-sizing: border-box;
@ -4232,6 +4224,28 @@ button.module-conversation-details__action-button {
}
}
.module-image--not-downloaded {
span:after {
content: '';
height: 24px;
width: 24px;
@include color-svg('../images/icons/v2/arrow-down-24.svg', $color-white);
}
}
.module-image--tap-to-play {
span:after {
content: 'GIF';
height: 24px;
width: 24px;
@include font-body-1;
line-height: 24px;
color: $color-white;
}
}
.module-image__download-pending {
position: relative;
@ -4336,6 +4350,26 @@ button.module-conversation-details__action-button {
}
}
.module-image--gif {
border-radius: 18px;
&__filesize {
position: absolute;
top: 10px;
left: 10px;
padding: 2px 8px;
color: $color-white;
background: $color-black-alpha-70;
/* The height is: 14px + 2x2px from the padding */
border-radius: 9px;
font-size: 11px;
line-height: 14px;
}
}
// Only if it's a sticker do we put the outline inside it
.module-message--selected
.module-message__container--with-sticker
@ -10677,6 +10711,11 @@ $contact-modal-padding: 18px;
&--deleted-for-everyone {
font-style: italic;
}
&--gif {
border-radius: inherit;
background: inherit;
}
}
.module-message__context {

View File

@ -39,6 +39,7 @@ $color-black-alpha-20: rgba($color-black, 0.2);
$color-black-alpha-40: rgba($color-black, 0.4);
$color-black-alpha-50: rgba($color-black, 0.5);
$color-black-alpha-60: rgba($color-black, 0.6);
$color-black-alpha-70: rgba($color-black, 0.7);
$color-black-alpha-80: rgba($color-black, 0.8);
$ultramarine-brand-light: #3a76f0;

View File

@ -0,0 +1,253 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useRef, useState, useEffect } from 'react';
import classNames from 'classnames';
import { Blurhash } from 'react-blurhash';
import formatFileSize from 'filesize';
import { LocalizerType, ThemeType } from '../../types/Util';
import { Spinner } from '../Spinner';
import {
AttachmentType,
hasNotDownloaded,
getImageDimensions,
defaultBlurHash,
} from '../../types/Attachment';
const MAX_GIF_REPEAT = 4;
const MAX_GIF_TIME = 8;
export type Props = {
readonly attachment: AttachmentType;
readonly size?: number;
readonly tabIndex: number;
readonly i18n: LocalizerType;
readonly theme?: ThemeType;
readonly reducedMotion?: boolean;
onError(): void;
kickOffAttachmentDownload(): void;
};
type MediaEvent = React.SyntheticEvent<HTMLVideoElement, Event>;
export const GIF: React.FC<Props> = props => {
const {
attachment,
size,
tabIndex,
i18n,
theme,
reducedMotion = Boolean(
window.Accessibility && window.Accessibility.reducedMotionSetting
),
onError,
kickOffAttachmentDownload,
} = props;
const tapToPlay = reducedMotion;
const videoRef = useRef<HTMLVideoElement | null>(null);
const { height, width } = getImageDimensions(attachment, size);
const [repeatCount, setRepeatCount] = useState(0);
const [playTime, setPlayTime] = useState(MAX_GIF_TIME);
const [currentTime, setCurrentTime] = useState(0);
const [isFocused, setIsFocused] = useState(true);
const [isPlaying, setIsPlaying] = useState(!tapToPlay);
useEffect(() => {
const onFocus = () => setIsFocused(true);
const onBlur = () => setIsFocused(false);
window.addEventListener('focus', onFocus);
window.addEventListener('blur', onBlur);
return () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('blur', onBlur);
};
});
//
// Play & Pause video in response to change of `isPlaying` and `repeatCount`.
//
useEffect(() => {
const { current: video } = videoRef;
if (!video) {
return;
}
if (isPlaying) {
video.play().catch(error => {
window.log.info(
"Failed to match GIF playback to window's state",
(error && error.stack) || error
);
});
} else {
video.pause();
}
}, [isPlaying, repeatCount]);
//
// Change `isPlaying` in response to focus, play time, and repeat count
// changes.
//
useEffect(() => {
const { current: video } = videoRef;
if (!video) {
return;
}
let isTapToPlayPaused = false;
if (tapToPlay) {
if (
playTime + currentTime >= MAX_GIF_TIME ||
repeatCount >= MAX_GIF_REPEAT
) {
isTapToPlayPaused = true;
}
}
setIsPlaying(isFocused && !isTapToPlayPaused);
}, [isFocused, playTime, currentTime, repeatCount, tapToPlay]);
const onTimeUpdate = async (event: MediaEvent): Promise<void> => {
const { currentTime: reportedTime } = event.currentTarget;
if (!Number.isNaN(reportedTime)) {
setCurrentTime(reportedTime);
}
};
const onEnded = async (event: MediaEvent): Promise<void> => {
const { currentTarget: video } = event;
const { duration } = video;
setRepeatCount(repeatCount + 1);
if (!Number.isNaN(duration)) {
video.currentTime = 0;
setCurrentTime(0);
setPlayTime(playTime + duration);
}
};
const onOverlayClick = (event: React.MouseEvent): void => {
event.preventDefault();
event.stopPropagation();
if (!attachment.url) {
kickOffAttachmentDownload();
} else if (tapToPlay) {
setPlayTime(0);
setCurrentTime(0);
setRepeatCount(0);
}
};
const onOverlayKeyDown = (event: React.KeyboardEvent): void => {
if (event.key !== 'Enter' && event.key !== 'Space') {
return;
}
event.preventDefault();
event.stopPropagation();
kickOffAttachmentDownload();
};
const isPending = Boolean(attachment.pending);
const isNotDownloaded = hasNotDownloaded(attachment) && !isPending;
let fileSize: JSX.Element | undefined;
if (isNotDownloaded && attachment.fileSize) {
fileSize = (
<div className="module-image--gif__filesize">
{formatFileSize(attachment.fileSize || 0)} · GIF
</div>
);
}
let gif: JSX.Element | undefined;
if (isNotDownloaded || isPending) {
gif = (
<Blurhash
hash={attachment.blurHash || defaultBlurHash(theme)}
width={width}
height={height}
style={{ display: 'block' }}
/>
);
} else {
gif = (
<video
ref={videoRef}
onTimeUpdate={onTimeUpdate}
onEnded={onEnded}
onError={onError}
className="module-image--gif__video"
autoPlay
playsInline
muted
poster={attachment.screenshot && attachment.screenshot.url}
height={height}
width={width}
src={attachment.url}
/>
);
}
let overlay: JSX.Element | undefined;
if ((tapToPlay && !isPlaying) || isNotDownloaded) {
const className = classNames([
'module-image__border-overlay',
'module-image__border-overlay--with-click-handler',
'module-image--soft-corners',
isNotDownloaded
? 'module-image--not-downloaded'
: 'module-image--tap-to-play',
]);
overlay = (
<button
type="button"
className={className}
onClick={onOverlayClick}
onKeyDown={onOverlayKeyDown}
tabIndex={tabIndex}
>
<span />
</button>
);
}
let spinner: JSX.Element | undefined;
if (isPending) {
spinner = (
<div className="module-image__download-pending--spinner-container">
<div
className="module-image__download-pending--spinner"
title={i18n('loading')}
>
<Spinner moduleClassName="module-image-spinner" svgSize="small" />
</div>
</div>
);
}
return (
<div className="module-image module-image--gif">
{gif}
{overlay}
{spinner}
{fileSize}
</div>
);
};

View File

@ -7,7 +7,11 @@ import { Blurhash } from 'react-blurhash';
import { Spinner } from '../Spinner';
import { LocalizerType, ThemeType } from '../../types/Util';
import { AttachmentType, hasNotDownloaded } from '../../types/Attachment';
import {
AttachmentType,
hasNotDownloaded,
defaultBlurHash,
} from '../../types/Attachment';
export type Props = {
alt: string;
@ -160,11 +164,7 @@ export class Image extends React.Component<Props> {
const canClick = this.canClick();
const imgNotDownloaded = hasNotDownloaded(attachment);
const defaulBlurHash =
theme === ThemeType.dark
? 'L05OQnoffQofoffQfQfQfQfQfQfQ'
: 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ';
const resolvedBlurHash = blurHash || defaulBlurHash;
const resolvedBlurHash = blurHash || defaultBlurHash(theme);
const overlayClassName = classNames('module-image__border-overlay', {
'module-image__border-overlay--with-border': !noBorder,
@ -189,7 +189,7 @@ export class Image extends React.Component<Props> {
onKeyDown={this.handleKeyDown}
tabIndex={tabIndex}
>
{imgNotDownloaded ? <i /> : null}
{imgNotDownloaded ? <span /> : null}
</button>
) : null;

View File

@ -8,6 +8,7 @@ import { action } from '@storybook/addon-actions';
import { boolean, number, text, select } from '@storybook/addon-knobs';
import { storiesOf } from '@storybook/react';
import { SignalService } from '../../protobuf';
import { Colors } from '../../types/Colors';
import { EmojiPicker } from '../emoji/EmojiPicker';
import { Message, Props, AudioAttachmentProps } from './Message';
@ -78,6 +79,7 @@ const createAuthorProp = (
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
attachments: overrideProps.attachments,
author: overrideProps.author || createAuthorProp(),
reducedMotion: boolean('reducedMotion', false),
bodyRanges: overrideProps.bodyRanges,
canReply: true,
canDownload: true,
@ -729,6 +731,63 @@ story.add('Image with Caption', () => {
return renderBothDirections(props);
});
story.add('GIF', () => {
const props = createProps({
attachments: [
{
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
url: '/fixtures/cat-gif.mp4',
width: 400,
height: 332,
},
],
status: 'sent',
});
return renderBothDirections(props);
});
story.add('Not Downloaded GIF', () => {
const props = createProps({
attachments: [
{
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
fileSize: 188610,
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 400,
height: 332,
},
],
status: 'sent',
});
return renderBothDirections(props);
});
story.add('Pending GIF', () => {
const props = createProps({
attachments: [
{
pending: true,
contentType: VIDEO_MP4,
flags: SignalService.AttachmentPointer.Flags.GIF,
fileName: 'cat-gif.mp4',
fileSize: 188610,
blurHash: 'LDA,FDBnm+I=p{tkIUI;~UkpELV]',
width: 400,
height: 332,
},
],
status: 'sent',
});
return renderBothDirections(props);
});
story.add('Audio', () => {
const props = createProps({
attachments: [

View File

@ -14,6 +14,7 @@ import { Spinner } from '../Spinner';
import { MessageBody } from './MessageBody';
import { ExpireTimer } from './ExpireTimer';
import { ImageGrid } from './ImageGrid';
import { GIF } from './GIF';
import { Image } from './Image';
import { Timestamp } from './Timestamp';
import { ContactName } from './ContactName';
@ -42,6 +43,7 @@ import {
isImage,
isImageAttachment,
isVideo,
isGIF,
} from '../../types/Attachment';
import { ContactType } from '../../types/Contact';
@ -58,6 +60,7 @@ type Trigger = {
};
const STICKER_SIZE = 200;
const GIF_SIZE = 300;
const SELECTED_TIMEOUT = 1000;
const THREE_HOURS = 3 * 60 * 60 * 1000;
@ -116,6 +119,7 @@ export type PropsData = {
| 'profileName'
| 'title'
>;
reducedMotion?: boolean;
conversationType: ConversationTypesType;
attachments?: Array<AttachmentType>;
quote?: {
@ -696,6 +700,7 @@ export class Message extends React.Component<Props, State> {
isSticker,
text,
theme,
reducedMotion,
renderAudioAttachment,
} = this.props;
@ -714,52 +719,72 @@ export class Message extends React.Component<Props, State> {
(conversationType === 'group' && direction === 'incoming');
const displayImage = canDisplayImage(attachments);
if (
displayImage &&
!imageBroken &&
(isImage(attachments) || isVideo(attachments))
) {
if (displayImage && !imageBroken) {
const prefix = isSticker ? 'sticker' : 'attachment';
const bottomOverlay = !isSticker && !collapseMetadata;
// We only want users to tab into this if there's more than one
const tabIndex = attachments.length > 1 ? 0 : -1;
return (
<div
className={classNames(
`module-message__${prefix}-container`,
withContentAbove
? `module-message__${prefix}-container--with-content-above`
: null,
withContentBelow
? 'module-message__attachment-container--with-content-below'
: null,
isSticker && !collapseMetadata
? 'module-message__sticker-container--with-content-below'
: null
)}
>
<ImageGrid
attachments={attachments}
withContentAbove={isSticker || withContentAbove}
withContentBelow={isSticker || withContentBelow}
isSticker={isSticker}
stickerSize={STICKER_SIZE}
bottomOverlay={bottomOverlay}
i18n={i18n}
theme={theme}
onError={this.handleImageError}
tabIndex={tabIndex}
onClick={attachment => {
if (hasNotDownloaded(attachment)) {
kickOffAttachmentDownload({ attachment, messageId: id });
} else {
showVisualAttachment({ attachment, messageId: id });
}
}}
/>
</div>
const containerClassName = classNames(
`module-message__${prefix}-container`,
withContentAbove
? `module-message__${prefix}-container--with-content-above`
: null,
withContentBelow
? 'module-message__attachment-container--with-content-below'
: null,
isSticker && !collapseMetadata
? 'module-message__sticker-container--with-content-below'
: null
);
if (isGIF(attachments)) {
return (
<div className={containerClassName}>
<GIF
attachment={firstAttachment}
size={GIF_SIZE}
theme={theme}
i18n={i18n}
tabIndex={0}
reducedMotion={reducedMotion}
onError={this.handleImageError}
kickOffAttachmentDownload={() => {
kickOffAttachmentDownload({
attachment: firstAttachment,
messageId: id,
});
}}
/>
</div>
);
}
if (isImage(attachments) || isVideo(attachments)) {
const bottomOverlay = !isSticker && !collapseMetadata;
// We only want users to tab into this if there's more than one
const tabIndex = attachments.length > 1 ? 0 : -1;
return (
<div className={containerClassName}>
<ImageGrid
attachments={attachments}
withContentAbove={isSticker || withContentAbove}
withContentBelow={isSticker || withContentBelow}
isSticker={isSticker}
stickerSize={STICKER_SIZE}
bottomOverlay={bottomOverlay}
i18n={i18n}
theme={theme}
onError={this.handleImageError}
tabIndex={tabIndex}
onClick={attachment => {
if (hasNotDownloaded(attachment)) {
kickOffAttachmentDownload({ attachment, messageId: id });
} else {
showVisualAttachment({ attachment, messageId: id });
}
}}
/>
</div>
);
}
}
if (isAudio(attachments)) {
return renderAudioAttachment({
@ -1553,6 +1578,11 @@ export class Message extends React.Component<Props, State> {
const { attachments, isSticker, previews } = this.props;
if (attachments && attachments.length) {
if (isGIF(attachments)) {
// Message container border + image border
return GIF_SIZE + 4;
}
if (isSticker) {
// Padding is 8px, on both sides, plus two for 1px border
return STICKER_SIZE + 8 * 2 + 2;
@ -2009,6 +2039,11 @@ export class Message extends React.Component<Props, State> {
const isAttachmentPending = this.isAttachmentPending();
// Don't show lightbox for GIFs
if (isGIF(attachments)) {
return;
}
if (isTapToView) {
if (isAttachmentPending) {
return;
@ -2186,6 +2221,7 @@ export class Message extends React.Component<Props, State> {
public renderContainer(): JSX.Element {
const {
attachments,
author,
deletedForEveryone,
direction,
@ -2203,6 +2239,7 @@ export class Message extends React.Component<Props, State> {
const containerClassnames = classNames(
'module-message__container',
isGIF(attachments) ? 'module-message__container--gif' : null,
isSelected && !isSticker ? 'module-message__container--selected' : null,
isSticker ? 'module-message__container--with-sticker' : null,
!isSticker ? `module-message__container--${direction}` : null,

View File

@ -1477,7 +1477,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const attachment = attachments[0] || {};
const { contentType } = attachment;
if (contentType === MIME.IMAGE_GIF) {
if (contentType === MIME.IMAGE_GIF || Attachment.isGIF(attachments)) {
return {
text: body || window.i18n('message--getNotificationText--gif'),
emoji: '🎡',

View File

@ -11,7 +11,7 @@ import {
isImageTypeSupported,
isVideoTypeSupported,
} from '../util/GoogleChrome';
import { LocalizerType } from './Util';
import { LocalizerType, ThemeType } from './Util';
const MAX_WIDTH = 300;
const MAX_HEIGHT = MAX_WIDTH * 1.5;
@ -30,7 +30,7 @@ export type AttachmentType = {
/** For messages not already on disk, this will be a data url */
url?: string;
size?: number;
fileSize?: string;
fileSize?: number;
pending?: boolean;
width?: number;
height?: number;
@ -157,20 +157,33 @@ export function hasImage(
);
}
export function isVideo(
attachments?: Array<AttachmentType>
): boolean | undefined {
return attachments && isVideoAttachment(attachments[0]);
export function isVideo(attachments?: Array<AttachmentType>): boolean {
if (!attachments || attachments.length === 0) {
return false;
}
return isVideoAttachment(attachments[0]);
}
export function isVideoAttachment(
attachment?: AttachmentType
): boolean | undefined {
return (
attachment &&
attachment.contentType &&
isVideoTypeSupported(attachment.contentType)
);
export function isVideoAttachment(attachment?: AttachmentType): boolean {
if (!attachment || !attachment.contentType) {
return false;
}
return isVideoTypeSupported(attachment.contentType);
}
export function isGIF(attachments?: ReadonlyArray<AttachmentType>): boolean {
if (!attachments || attachments.length !== 1) {
return false;
}
const [attachment] = attachments;
const flag = SignalService.AttachmentPointer.Flags.GIF;
const hasFlag =
// eslint-disable-next-line no-bitwise
!is.undefined(attachment.flags) && (attachment.flags & flag) === flag;
return hasFlag && isVideoAttachment(attachment);
}
export function hasNotDownloaded(attachment?: AttachmentType): boolean {
@ -280,9 +293,10 @@ export function getAlt(
attachment: AttachmentType,
i18n: LocalizerType
): string {
return isVideoAttachment(attachment)
? i18n('videoAttachmentAlt')
: i18n('imageAttachmentAlt');
if (isVideoAttachment(attachment)) {
return i18n('videoAttachmentAlt');
}
return i18n('imageAttachmentAlt');
}
// Migration-related attachment stuff
@ -445,3 +459,10 @@ export const getUploadSizeLimitKb = (contentType: MIME.MIMEType): number => {
}
return 100000;
};
export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => {
if (theme === ThemeType.dark) {
return 'L05OQnoffQofoffQfQfQfQfQfQfQ';
}
return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ';
};

View File

@ -16524,6 +16524,15 @@
"updated": "2020-10-26T19:12:24.410Z",
"reasonDetail": "Doesn't refer to a DOM element."
},
{
"rule": "React-useRef",
"path": "ts/components/conversation/GIF.js",
"line": " const videoRef = react_1.useRef(null);",
"lineNumber": 39,
"reasonCategory": "usageTrusted",
"updated": "2021-04-17T01:47:31.419Z",
"reasonDetail": "Used for managing playback of GIF video"
},
{
"rule": "React-createRef",
"path": "ts/components/conversation/InlineNotificationWrapper.js",
@ -16546,7 +16555,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.js",
"line": " this.focusRef = react_1.default.createRef();",
"lineNumber": 73,
"lineNumber": 75,
"reasonCategory": "usageTrusted",
"updated": "2021-03-05T20:05:07.474Z",
"reasonDetail": "Used for managing focus only"
@ -16555,7 +16564,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.js",
"line": " this.audioButtonRef = react_1.default.createRef();",
"lineNumber": 74,
"lineNumber": 76,
"reasonCategory": "usageTrusted",
"updated": "2021-03-05T20:05:07.474Z",
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
@ -16564,7 +16573,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.js",
"line": " this.reactionsContainerRef = react_1.default.createRef();",
"lineNumber": 75,
"lineNumber": 77,
"reasonCategory": "usageTrusted",
"updated": "2020-08-28T16:12:19.904Z",
"reasonDetail": "Used for detecting clicks outside reaction viewer"
@ -16573,7 +16582,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 246,
"lineNumber": 250,
"reasonCategory": "usageTrusted",
"updated": "2021-03-05T19:57:01.431Z",
"reasonDetail": "Used for managing focus only"
@ -16582,7 +16591,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
"lineNumber": 248,
"lineNumber": 252,
"reasonCategory": "usageTrusted",
"updated": "2021-03-05T19:57:01.431Z",
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
@ -16591,7 +16600,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public reactionsContainerRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 250,
"lineNumber": 254,
"reasonCategory": "usageTrusted",
"updated": "2021-03-05T19:57:01.431Z",
"reasonDetail": "Used for detecting clicks outside reaction viewer"

4
ts/window.d.ts vendored
View File

@ -263,6 +263,9 @@ declare global {
deviceName: string;
}
| undefined;
Accessibility: {
reducedMotionSetting: boolean;
};
Signal: {
Backbone: any;
AttachmentDownloads: {
@ -376,6 +379,7 @@ declare global {
isVoiceMessage: (attachments: unknown) => boolean;
isImage: typeof Attachment.isImage;
isGIF: typeof Attachment.isGIF;
isVideo: typeof Attachment.isVideo;
isAudio: typeof Attachment.isAudio;