GIF attachments
This commit is contained in:
parent
5f17d01f49
commit
caf1d4c4da
15 changed files with 526 additions and 93 deletions
BIN
fixtures/cat-gif.mp4
Normal file
BIN
fixtures/cat-gif.mp4
Normal file
Binary file not shown.
BIN
fixtures/cat-screenshot.png
Normal file
BIN
fixtures/cat-screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 182 KiB |
|
@ -215,6 +215,7 @@ exports.deleteData = deleteOnDisk => {
|
||||||
|
|
||||||
exports.isImage = AttachmentTS.isImage;
|
exports.isImage = AttachmentTS.isImage;
|
||||||
exports.isVideo = AttachmentTS.isVideo;
|
exports.isVideo = AttachmentTS.isVideo;
|
||||||
|
exports.isGIF = AttachmentTS.isGIF;
|
||||||
exports.isAudio = AttachmentTS.isAudio;
|
exports.isAudio = AttachmentTS.isAudio;
|
||||||
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
||||||
exports.getUploadSizeLimitKb = AttachmentTS.getUploadSizeLimitKb;
|
exports.getUploadSizeLimitKb = AttachmentTS.getUploadSizeLimitKb;
|
||||||
|
|
5
main.js
5
main.js
|
@ -42,6 +42,8 @@ const {
|
||||||
systemPreferences,
|
systemPreferences,
|
||||||
} = electron;
|
} = electron;
|
||||||
|
|
||||||
|
const animationSettings = systemPreferences.getAnimationSettings();
|
||||||
|
|
||||||
const appUserModelId = `org.whispersystems.${packageJson.name}`;
|
const appUserModelId = `org.whispersystems.${packageJson.name}`;
|
||||||
console.log('Set Windows Application User Model ID (AUMID)', {
|
console.log('Set Windows Application User Model ID (AUMID)', {
|
||||||
appUserModelId,
|
appUserModelId,
|
||||||
|
@ -245,6 +247,9 @@ function prepareURL(pathSegments, moreKeys) {
|
||||||
contentProxyUrl: config.contentProxyUrl,
|
contentProxyUrl: config.contentProxyUrl,
|
||||||
sfuUrl: config.get('sfuUrl'),
|
sfuUrl: config.get('sfuUrl'),
|
||||||
importMode: importMode ? true : undefined, // for stringify()
|
importMode: importMode ? true : undefined, // for stringify()
|
||||||
|
reducedMotionSetting: animationSettings.prefersReducedMotion
|
||||||
|
? true
|
||||||
|
: undefined,
|
||||||
serverPublicParams: config.get('serverPublicParams'),
|
serverPublicParams: config.get('serverPublicParams'),
|
||||||
serverTrustRoot: config.get('serverTrustRoot'),
|
serverTrustRoot: config.get('serverTrustRoot'),
|
||||||
appStartInitialSpellcheckSetting,
|
appStartInitialSpellcheckSetting,
|
||||||
|
|
|
@ -476,6 +476,10 @@ try {
|
||||||
activeWindowService
|
activeWindowService
|
||||||
);
|
);
|
||||||
|
|
||||||
|
window.Accessibility = {
|
||||||
|
reducedMotionSetting: Boolean(config.reducedMotionSetting),
|
||||||
|
};
|
||||||
|
|
||||||
window.isValidGuid = maybeGuid =>
|
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(
|
/^[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
|
maybeGuid
|
||||||
|
|
|
@ -663,6 +663,10 @@
|
||||||
border-top-left-radius: 0px;
|
border-top-left-radius: 0px;
|
||||||
border-top-right-radius: 0px;
|
border-top-right-radius: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-message__container--gif & {
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__sticker-container {
|
.module-message__sticker-container {
|
||||||
|
@ -4188,42 +4192,30 @@ button.module-conversation-details__action-button {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-image--tap-to-play,
|
||||||
.module-image--not-downloaded {
|
.module-image--not-downloaded {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
i {
|
span {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 48px;
|
border-radius: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
background-color: $color-black-alpha-70;
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
i {
|
span {
|
||||||
background-color: $color-black;
|
background-color: $color-black-alpha-80;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
i {
|
span {
|
||||||
background-color: $color-gray-75;
|
background-color: $color-gray-75;
|
||||||
border: 4px solid $ultramarine-ui-light;
|
border: 4px solid $ultramarine-ui-light;
|
||||||
box-sizing: border-box;
|
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 {
|
.module-image__download-pending {
|
||||||
position: relative;
|
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
|
// Only if it's a sticker do we put the outline inside it
|
||||||
.module-message--selected
|
.module-message--selected
|
||||||
.module-message__container--with-sticker
|
.module-message__container--with-sticker
|
||||||
|
@ -10677,6 +10711,11 @@ $contact-modal-padding: 18px;
|
||||||
&--deleted-for-everyone {
|
&--deleted-for-everyone {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&--gif {
|
||||||
|
border-radius: inherit;
|
||||||
|
background: inherit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-message__context {
|
.module-message__context {
|
||||||
|
|
|
@ -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-40: rgba($color-black, 0.4);
|
||||||
$color-black-alpha-50: rgba($color-black, 0.5);
|
$color-black-alpha-50: rgba($color-black, 0.5);
|
||||||
$color-black-alpha-60: rgba($color-black, 0.6);
|
$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);
|
$color-black-alpha-80: rgba($color-black, 0.8);
|
||||||
|
|
||||||
$ultramarine-brand-light: #3a76f0;
|
$ultramarine-brand-light: #3a76f0;
|
||||||
|
|
253
ts/components/conversation/GIF.tsx
Normal file
253
ts/components/conversation/GIF.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -7,7 +7,11 @@ import { Blurhash } from 'react-blurhash';
|
||||||
|
|
||||||
import { Spinner } from '../Spinner';
|
import { Spinner } from '../Spinner';
|
||||||
import { LocalizerType, ThemeType } from '../../types/Util';
|
import { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
import { AttachmentType, hasNotDownloaded } from '../../types/Attachment';
|
import {
|
||||||
|
AttachmentType,
|
||||||
|
hasNotDownloaded,
|
||||||
|
defaultBlurHash,
|
||||||
|
} from '../../types/Attachment';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
alt: string;
|
alt: string;
|
||||||
|
@ -160,11 +164,7 @@ export class Image extends React.Component<Props> {
|
||||||
const canClick = this.canClick();
|
const canClick = this.canClick();
|
||||||
const imgNotDownloaded = hasNotDownloaded(attachment);
|
const imgNotDownloaded = hasNotDownloaded(attachment);
|
||||||
|
|
||||||
const defaulBlurHash =
|
const resolvedBlurHash = blurHash || defaultBlurHash(theme);
|
||||||
theme === ThemeType.dark
|
|
||||||
? 'L05OQnoffQofoffQfQfQfQfQfQfQ'
|
|
||||||
: 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ';
|
|
||||||
const resolvedBlurHash = blurHash || defaulBlurHash;
|
|
||||||
|
|
||||||
const overlayClassName = classNames('module-image__border-overlay', {
|
const overlayClassName = classNames('module-image__border-overlay', {
|
||||||
'module-image__border-overlay--with-border': !noBorder,
|
'module-image__border-overlay--with-border': !noBorder,
|
||||||
|
@ -189,7 +189,7 @@ export class Image extends React.Component<Props> {
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
tabIndex={tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
{imgNotDownloaded ? <i /> : null}
|
{imgNotDownloaded ? <span /> : null}
|
||||||
</button>
|
</button>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { action } from '@storybook/addon-actions';
|
||||||
import { boolean, number, text, select } from '@storybook/addon-knobs';
|
import { boolean, number, text, select } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
|
import { SignalService } from '../../protobuf';
|
||||||
import { Colors } from '../../types/Colors';
|
import { Colors } from '../../types/Colors';
|
||||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||||
import { Message, Props, AudioAttachmentProps } from './Message';
|
import { Message, Props, AudioAttachmentProps } from './Message';
|
||||||
|
@ -78,6 +79,7 @@ const createAuthorProp = (
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
attachments: overrideProps.attachments,
|
attachments: overrideProps.attachments,
|
||||||
author: overrideProps.author || createAuthorProp(),
|
author: overrideProps.author || createAuthorProp(),
|
||||||
|
reducedMotion: boolean('reducedMotion', false),
|
||||||
bodyRanges: overrideProps.bodyRanges,
|
bodyRanges: overrideProps.bodyRanges,
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
|
@ -729,6 +731,63 @@ story.add('Image with Caption', () => {
|
||||||
return renderBothDirections(props);
|
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', () => {
|
story.add('Audio', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
attachments: [
|
attachments: [
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { Spinner } from '../Spinner';
|
||||||
import { MessageBody } from './MessageBody';
|
import { MessageBody } from './MessageBody';
|
||||||
import { ExpireTimer } from './ExpireTimer';
|
import { ExpireTimer } from './ExpireTimer';
|
||||||
import { ImageGrid } from './ImageGrid';
|
import { ImageGrid } from './ImageGrid';
|
||||||
|
import { GIF } from './GIF';
|
||||||
import { Image } from './Image';
|
import { Image } from './Image';
|
||||||
import { Timestamp } from './Timestamp';
|
import { Timestamp } from './Timestamp';
|
||||||
import { ContactName } from './ContactName';
|
import { ContactName } from './ContactName';
|
||||||
|
@ -42,6 +43,7 @@ import {
|
||||||
isImage,
|
isImage,
|
||||||
isImageAttachment,
|
isImageAttachment,
|
||||||
isVideo,
|
isVideo,
|
||||||
|
isGIF,
|
||||||
} from '../../types/Attachment';
|
} from '../../types/Attachment';
|
||||||
import { ContactType } from '../../types/Contact';
|
import { ContactType } from '../../types/Contact';
|
||||||
|
|
||||||
|
@ -58,6 +60,7 @@ type Trigger = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const STICKER_SIZE = 200;
|
const STICKER_SIZE = 200;
|
||||||
|
const GIF_SIZE = 300;
|
||||||
const SELECTED_TIMEOUT = 1000;
|
const SELECTED_TIMEOUT = 1000;
|
||||||
const THREE_HOURS = 3 * 60 * 60 * 1000;
|
const THREE_HOURS = 3 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
@ -116,6 +119,7 @@ export type PropsData = {
|
||||||
| 'profileName'
|
| 'profileName'
|
||||||
| 'title'
|
| 'title'
|
||||||
>;
|
>;
|
||||||
|
reducedMotion?: boolean;
|
||||||
conversationType: ConversationTypesType;
|
conversationType: ConversationTypesType;
|
||||||
attachments?: Array<AttachmentType>;
|
attachments?: Array<AttachmentType>;
|
||||||
quote?: {
|
quote?: {
|
||||||
|
@ -696,6 +700,7 @@ export class Message extends React.Component<Props, State> {
|
||||||
isSticker,
|
isSticker,
|
||||||
text,
|
text,
|
||||||
theme,
|
theme,
|
||||||
|
reducedMotion,
|
||||||
|
|
||||||
renderAudioAttachment,
|
renderAudioAttachment,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -714,52 +719,72 @@ export class Message extends React.Component<Props, State> {
|
||||||
(conversationType === 'group' && direction === 'incoming');
|
(conversationType === 'group' && direction === 'incoming');
|
||||||
const displayImage = canDisplayImage(attachments);
|
const displayImage = canDisplayImage(attachments);
|
||||||
|
|
||||||
if (
|
if (displayImage && !imageBroken) {
|
||||||
displayImage &&
|
|
||||||
!imageBroken &&
|
|
||||||
(isImage(attachments) || isVideo(attachments))
|
|
||||||
) {
|
|
||||||
const prefix = isSticker ? 'sticker' : 'attachment';
|
const prefix = isSticker ? 'sticker' : 'attachment';
|
||||||
const bottomOverlay = !isSticker && !collapseMetadata;
|
const containerClassName = classNames(
|
||||||
// We only want users to tab into this if there's more than one
|
`module-message__${prefix}-container`,
|
||||||
const tabIndex = attachments.length > 1 ? 0 : -1;
|
withContentAbove
|
||||||
|
? `module-message__${prefix}-container--with-content-above`
|
||||||
return (
|
: null,
|
||||||
<div
|
withContentBelow
|
||||||
className={classNames(
|
? 'module-message__attachment-container--with-content-below'
|
||||||
`module-message__${prefix}-container`,
|
: null,
|
||||||
withContentAbove
|
isSticker && !collapseMetadata
|
||||||
? `module-message__${prefix}-container--with-content-above`
|
? 'module-message__sticker-container--with-content-below'
|
||||||
: null,
|
: 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>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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)) {
|
if (isAudio(attachments)) {
|
||||||
return renderAudioAttachment({
|
return renderAudioAttachment({
|
||||||
|
@ -1553,6 +1578,11 @@ export class Message extends React.Component<Props, State> {
|
||||||
const { attachments, isSticker, previews } = this.props;
|
const { attachments, isSticker, previews } = this.props;
|
||||||
|
|
||||||
if (attachments && attachments.length) {
|
if (attachments && attachments.length) {
|
||||||
|
if (isGIF(attachments)) {
|
||||||
|
// Message container border + image border
|
||||||
|
return GIF_SIZE + 4;
|
||||||
|
}
|
||||||
|
|
||||||
if (isSticker) {
|
if (isSticker) {
|
||||||
// Padding is 8px, on both sides, plus two for 1px border
|
// Padding is 8px, on both sides, plus two for 1px border
|
||||||
return STICKER_SIZE + 8 * 2 + 2;
|
return STICKER_SIZE + 8 * 2 + 2;
|
||||||
|
@ -2009,6 +2039,11 @@ export class Message extends React.Component<Props, State> {
|
||||||
|
|
||||||
const isAttachmentPending = this.isAttachmentPending();
|
const isAttachmentPending = this.isAttachmentPending();
|
||||||
|
|
||||||
|
// Don't show lightbox for GIFs
|
||||||
|
if (isGIF(attachments)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isTapToView) {
|
if (isTapToView) {
|
||||||
if (isAttachmentPending) {
|
if (isAttachmentPending) {
|
||||||
return;
|
return;
|
||||||
|
@ -2186,6 +2221,7 @@ export class Message extends React.Component<Props, State> {
|
||||||
|
|
||||||
public renderContainer(): JSX.Element {
|
public renderContainer(): JSX.Element {
|
||||||
const {
|
const {
|
||||||
|
attachments,
|
||||||
author,
|
author,
|
||||||
deletedForEveryone,
|
deletedForEveryone,
|
||||||
direction,
|
direction,
|
||||||
|
@ -2203,6 +2239,7 @@ export class Message extends React.Component<Props, State> {
|
||||||
|
|
||||||
const containerClassnames = classNames(
|
const containerClassnames = classNames(
|
||||||
'module-message__container',
|
'module-message__container',
|
||||||
|
isGIF(attachments) ? 'module-message__container--gif' : null,
|
||||||
isSelected && !isSticker ? 'module-message__container--selected' : null,
|
isSelected && !isSticker ? 'module-message__container--selected' : null,
|
||||||
isSticker ? 'module-message__container--with-sticker' : null,
|
isSticker ? 'module-message__container--with-sticker' : null,
|
||||||
!isSticker ? `module-message__container--${direction}` : null,
|
!isSticker ? `module-message__container--${direction}` : null,
|
||||||
|
|
|
@ -1477,7 +1477,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const attachment = attachments[0] || {};
|
const attachment = attachments[0] || {};
|
||||||
const { contentType } = attachment;
|
const { contentType } = attachment;
|
||||||
|
|
||||||
if (contentType === MIME.IMAGE_GIF) {
|
if (contentType === MIME.IMAGE_GIF || Attachment.isGIF(attachments)) {
|
||||||
return {
|
return {
|
||||||
text: body || window.i18n('message--getNotificationText--gif'),
|
text: body || window.i18n('message--getNotificationText--gif'),
|
||||||
emoji: '🎡',
|
emoji: '🎡',
|
||||||
|
|
|
@ -11,7 +11,7 @@ import {
|
||||||
isImageTypeSupported,
|
isImageTypeSupported,
|
||||||
isVideoTypeSupported,
|
isVideoTypeSupported,
|
||||||
} from '../util/GoogleChrome';
|
} from '../util/GoogleChrome';
|
||||||
import { LocalizerType } from './Util';
|
import { LocalizerType, ThemeType } from './Util';
|
||||||
|
|
||||||
const MAX_WIDTH = 300;
|
const MAX_WIDTH = 300;
|
||||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
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 */
|
/** For messages not already on disk, this will be a data url */
|
||||||
url?: string;
|
url?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
fileSize?: string;
|
fileSize?: number;
|
||||||
pending?: boolean;
|
pending?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
|
@ -157,20 +157,33 @@ export function hasImage(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVideo(
|
export function isVideo(attachments?: Array<AttachmentType>): boolean {
|
||||||
attachments?: Array<AttachmentType>
|
if (!attachments || attachments.length === 0) {
|
||||||
): boolean | undefined {
|
return false;
|
||||||
return attachments && isVideoAttachment(attachments[0]);
|
}
|
||||||
|
return isVideoAttachment(attachments[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isVideoAttachment(
|
export function isVideoAttachment(attachment?: AttachmentType): boolean {
|
||||||
attachment?: AttachmentType
|
if (!attachment || !attachment.contentType) {
|
||||||
): boolean | undefined {
|
return false;
|
||||||
return (
|
}
|
||||||
attachment &&
|
return isVideoTypeSupported(attachment.contentType);
|
||||||
attachment.contentType &&
|
}
|
||||||
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 {
|
export function hasNotDownloaded(attachment?: AttachmentType): boolean {
|
||||||
|
@ -280,9 +293,10 @@ export function getAlt(
|
||||||
attachment: AttachmentType,
|
attachment: AttachmentType,
|
||||||
i18n: LocalizerType
|
i18n: LocalizerType
|
||||||
): string {
|
): string {
|
||||||
return isVideoAttachment(attachment)
|
if (isVideoAttachment(attachment)) {
|
||||||
? i18n('videoAttachmentAlt')
|
return i18n('videoAttachmentAlt');
|
||||||
: i18n('imageAttachmentAlt');
|
}
|
||||||
|
return i18n('imageAttachmentAlt');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration-related attachment stuff
|
// Migration-related attachment stuff
|
||||||
|
@ -445,3 +459,10 @@ export const getUploadSizeLimitKb = (contentType: MIME.MIMEType): number => {
|
||||||
}
|
}
|
||||||
return 100000;
|
return 100000;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultBlurHash = (theme: ThemeType = ThemeType.light): string => {
|
||||||
|
if (theme === ThemeType.dark) {
|
||||||
|
return 'L05OQnoffQofoffQfQfQfQfQfQfQ';
|
||||||
|
}
|
||||||
|
return 'L1Q]+w-;fQ-;~qfQfQfQfQfQfQfQ';
|
||||||
|
};
|
||||||
|
|
|
@ -16524,6 +16524,15 @@
|
||||||
"updated": "2020-10-26T19:12:24.410Z",
|
"updated": "2020-10-26T19:12:24.410Z",
|
||||||
"reasonDetail": "Doesn't refer to a DOM element."
|
"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",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/InlineNotificationWrapper.js",
|
"path": "ts/components/conversation/InlineNotificationWrapper.js",
|
||||||
|
@ -16546,7 +16555,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.js",
|
"path": "ts/components/conversation/Message.js",
|
||||||
"line": " this.focusRef = react_1.default.createRef();",
|
"line": " this.focusRef = react_1.default.createRef();",
|
||||||
"lineNumber": 73,
|
"lineNumber": 75,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T20:05:07.474Z",
|
"updated": "2021-03-05T20:05:07.474Z",
|
||||||
"reasonDetail": "Used for managing focus only"
|
"reasonDetail": "Used for managing focus only"
|
||||||
|
@ -16555,7 +16564,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.js",
|
"path": "ts/components/conversation/Message.js",
|
||||||
"line": " this.audioButtonRef = react_1.default.createRef();",
|
"line": " this.audioButtonRef = react_1.default.createRef();",
|
||||||
"lineNumber": 74,
|
"lineNumber": 76,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T20:05:07.474Z",
|
"updated": "2021-03-05T20:05:07.474Z",
|
||||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||||
|
@ -16564,7 +16573,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.js",
|
"path": "ts/components/conversation/Message.js",
|
||||||
"line": " this.reactionsContainerRef = react_1.default.createRef();",
|
"line": " this.reactionsContainerRef = react_1.default.createRef();",
|
||||||
"lineNumber": 75,
|
"lineNumber": 77,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-08-28T16:12:19.904Z",
|
"updated": "2020-08-28T16:12:19.904Z",
|
||||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||||
|
@ -16573,7 +16582,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 246,
|
"lineNumber": 250,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for managing focus only"
|
"reasonDetail": "Used for managing focus only"
|
||||||
|
@ -16582,7 +16591,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
||||||
"lineNumber": 248,
|
"lineNumber": 252,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||||
|
@ -16591,7 +16600,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public reactionsContainerRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public reactionsContainerRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 250,
|
"lineNumber": 254,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||||
|
|
4
ts/window.d.ts
vendored
4
ts/window.d.ts
vendored
|
@ -263,6 +263,9 @@ declare global {
|
||||||
deviceName: string;
|
deviceName: string;
|
||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
|
Accessibility: {
|
||||||
|
reducedMotionSetting: boolean;
|
||||||
|
};
|
||||||
Signal: {
|
Signal: {
|
||||||
Backbone: any;
|
Backbone: any;
|
||||||
AttachmentDownloads: {
|
AttachmentDownloads: {
|
||||||
|
@ -376,6 +379,7 @@ declare global {
|
||||||
|
|
||||||
isVoiceMessage: (attachments: unknown) => boolean;
|
isVoiceMessage: (attachments: unknown) => boolean;
|
||||||
isImage: typeof Attachment.isImage;
|
isImage: typeof Attachment.isImage;
|
||||||
|
isGIF: typeof Attachment.isGIF;
|
||||||
isVideo: typeof Attachment.isVideo;
|
isVideo: typeof Attachment.isVideo;
|
||||||
isAudio: typeof Attachment.isAudio;
|
isAudio: typeof Attachment.isAudio;
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue