GIF attachments
This commit is contained in:
parent
5f17d01f49
commit
caf1d4c4da
15 changed files with 526 additions and 93 deletions
fixtures
js/modules/types
main.jspreload.jsstylesheets
ts
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.isVideo = AttachmentTS.isVideo;
|
||||
exports.isGIF = AttachmentTS.isGIF;
|
||||
exports.isAudio = AttachmentTS.isAudio;
|
||||
exports.isVoiceMessage = AttachmentTS.isVoiceMessage;
|
||||
exports.getUploadSizeLimitKb = AttachmentTS.getUploadSizeLimitKb;
|
||||
|
|
5
main.js
5
main.js
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
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 { 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;
|
||||
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: '🎡',
|
||||
|
|
|
@ -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';
|
||||
};
|
||||
|
|
|
@ -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
4
ts/window.d.ts
vendored
|
@ -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;
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue