Fixes view once videos in lightbox

This commit is contained in:
Josh Perez 2021-08-24 17:47:14 -04:00 committed by GitHub
parent 425404cd6e
commit 28f5a2bd1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 104 additions and 67 deletions

View file

@ -41,6 +41,7 @@ $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-70: rgba($color-black, 0.7);
$color-black-alpha-80: rgba($color-black, 0.8); $color-black-alpha-80: rgba($color-black, 0.8);
$color-black-alpha-90: rgba($color-black, 0.9);
$color-ultramarine-dark: #1851b4; $color-ultramarine-dark: #1851b4;
$color-ultramarine-icon: #3a76f0; $color-ultramarine-icon: #3a76f0;

View file

@ -3,7 +3,7 @@
.Lightbox { .Lightbox {
&__container { &__container {
background-color: $color-black-alpha-80; background-color: $color-black-alpha-90;
bottom: 0; bottom: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -52,6 +52,7 @@ function createMediaItem(
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
close: action('close'), close: action('close'),
i18n, i18n,
isViewOnce: Boolean(overrideProps.isViewOnce),
media: overrideProps.media || [], media: overrideProps.media || [],
onSave: action('onSave'), onSave: action('onSave'),
selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0), selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0),
@ -288,3 +289,18 @@ story.add('Conversation Header', () => (
]} ]}
/> />
)); ));
story.add('View Once Video', () => (
<Lightbox
{...createProps({
isViewOnce: true,
media: [
createMediaItem({
contentType: VIDEO_MP4,
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
}),
],
})}
isViewOnce
/>
));

View file

@ -9,9 +9,10 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import moment from 'moment';
import classNames from 'classnames'; import classNames from 'classnames';
import moment from 'moment';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { noop } from 'lodash';
import * as GoogleChrome from '../util/GoogleChrome'; import * as GoogleChrome from '../util/GoogleChrome';
import { AttachmentType, isGIF } from '../types/Attachment'; import { AttachmentType, isGIF } from '../types/Attachment';
@ -20,12 +21,14 @@ import { ConversationType } from '../state/ducks/conversations';
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME'; import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { MediaItemType, MessageAttributesType } from '../types/MediaItem'; import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
import { formatDuration } from '../util/formatDuration';
export type PropsType = { export type PropsType = {
children?: ReactNode; children?: ReactNode;
close: () => void; close: () => void;
getConversation?: (id: string) => ConversationType; getConversation?: (id: string) => ConversationType;
i18n: LocalizerType; i18n: LocalizerType;
isViewOnce?: boolean;
media: Array<MediaItemType>; media: Array<MediaItemType>;
onForward?: (messageId: string) => void; onForward?: (messageId: string) => void;
onSave?: (options: { onSave?: (options: {
@ -42,20 +45,24 @@ export function Lightbox({
getConversation, getConversation,
media, media,
i18n, i18n,
isViewOnce = false,
onForward, onForward,
onSave, onSave,
selectedIndex: initialSelectedIndex, selectedIndex: initialSelectedIndex = 0,
}: PropsType): JSX.Element | null { }: PropsType): JSX.Element | null {
const [root, setRoot] = React.useState<HTMLElement | undefined>(); const [root, setRoot] = React.useState<HTMLElement | undefined>();
const [selectedIndex, setSelectedIndex] = useState<number>( const [selectedIndex, setSelectedIndex] = useState<number>(
initialSelectedIndex || 0 initialSelectedIndex
); );
const [previousFocus, setPreviousFocus] = useState<HTMLElement | undefined>(); const [previousFocus, setPreviousFocus] = useState<HTMLElement | undefined>();
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(
null
);
const [videoTime, setVideoTime] = useState<number | undefined>();
const [zoomed, setZoomed] = useState(false); const [zoomed, setZoomed] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
const focusRef = useRef<HTMLDivElement | null>(null); const focusRef = useRef<HTMLDivElement | null>(null);
const videoRef = useRef<HTMLVideoElement | null>(null);
const restorePreviousFocus = useCallback(() => { const restorePreviousFocus = useCallback(() => {
if (previousFocus && previousFocus.focus) { if (previousFocus && previousFocus.focus) {
@ -73,6 +80,13 @@ export function Lightbox({
); );
}, [media]); }, [media]);
const onTimeUpdate = useCallback(() => {
if (!videoElement) {
return;
}
setVideoTime(videoElement.currentTime);
}, [setVideoTime, videoElement]);
const handleSave = () => { const handleSave = () => {
const mediaItem = media[selectedIndex]; const mediaItem = media[selectedIndex];
const { attachment, message, index } = mediaItem; const { attachment, message, index } = mediaItem;
@ -130,18 +144,17 @@ export function Lightbox({
close(); close();
}; };
const playVideo = () => { const playVideo = useCallback(() => {
const video = videoRef.current; if (!videoElement) {
if (!video) {
return; return;
} }
if (video.paused) { if (videoElement.paused) {
video.play(); videoElement.play();
} else { } else {
video.pause(); videoElement.pause();
} }
}; }, [videoElement]);
useEffect(() => { useEffect(() => {
const div = document.createElement('div'); const div = document.createElement('div');
@ -176,22 +189,22 @@ export function Lightbox({
}, [onKeyDown]); }, [onKeyDown]);
useEffect(() => { useEffect(() => {
// Wait until we're added to the DOM. ConversationView first creates this playVideo();
// view, then appends its elements into the DOM.
const timeout = window.setTimeout(() => {
playVideo();
if (focusRef && focusRef.current) { if (focusRef && focusRef.current) {
focusRef.current.focus(); focusRef.current.focus();
} }
});
return () => { if (videoElement && isViewOnce) {
if (timeout) { videoElement.addEventListener('timeupdate', onTimeUpdate);
window.clearTimeout(timeout);
} return () => {
}; videoElement.removeEventListener('timeupdate', onTimeUpdate);
}, [selectedIndex]); };
}
return noop;
}, [isViewOnce, onTimeUpdate, playVideo, videoElement]);
const { attachment, contentType, loop = false, objectURL, message } = const { attachment, contentType, loop = false, objectURL, message } =
media[selectedIndex] || {}; media[selectedIndex] || {};
@ -248,14 +261,14 @@ export function Lightbox({
); );
} }
} else if (isVideoTypeSupported) { } else if (isVideoTypeSupported) {
const shouldLoop = loop || isGIF([attachment]); const shouldLoop = loop || isGIF([attachment]) || isViewOnce;
content = ( content = (
<video <video
className="Lightbox__object" className="Lightbox__object"
controls={!shouldLoop} controls={!shouldLoop}
key={objectURL} key={objectURL}
loop={shouldLoop} loop={shouldLoop}
ref={videoRef} ref={setVideoElement}
> >
<source src={objectURL} /> <source src={objectURL} />
</video> </video>
@ -388,6 +401,11 @@ export function Lightbox({
</div> </div>
{!zoomed && ( {!zoomed && (
<div className="Lightbox__footer"> <div className="Lightbox__footer">
{isViewOnce && videoTime ? (
<div className="Lightbox__timestamp">
{formatDuration(videoTime)}
</div>
) : null}
{caption ? ( {caption ? (
<div className="Lightbox__caption">{caption}</div> <div className="Lightbox__caption">{caption}</div>
) : null} ) : null}

View file

@ -13535,13 +13535,6 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-08-23T18:39:37.081Z" "updated": "2021-08-23T18:39:37.081Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/Lightbox.js",
"line": " const videoRef = react_1.useRef(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-08-23T18:39:37.081Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/Lightbox.tsx", "path": "ts/components/Lightbox.tsx",
@ -13556,13 +13549,6 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-08-23T18:39:37.081Z" "updated": "2021-08-23T18:39:37.081Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/Lightbox.tsx",
"line": " const videoRef = useRef<HTMLVideoElement | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2021-08-23T18:39:37.081Z"
},
{ {
"rule": "React-createRef", "rule": "React-createRef",
"path": "ts/components/MainHeader.js", "path": "ts/components/MainHeader.js",

View file

@ -2602,7 +2602,19 @@ Whisper.ConversationView = Whisper.View.extend({
contentType: attachment.contentType, contentType: attachment.contentType,
index, index,
attachment, attachment,
message, message: {
attachments: message.attachments || [],
conversationId:
window.ConversationController.get(
window.ConversationController.ensureContactIds({
uuid: message.sourceUuid,
e164: message.source,
})
)?.id || message.conversationId,
id: message.id,
received_at: message.received_at,
received_at_ms: Number(message.received_at_ms),
},
}; };
}); });
}) })
@ -2652,22 +2664,9 @@ Whisper.ConversationView = Whisper.View.extend({
} }
case 'media': { case 'media': {
const selectedIndex = media.findIndex( const selectedMedia =
mediaMessage => mediaMessage.attachment.path === attachment.path media.find(item => attachment.path === item.path) || media[0];
); this.showLightboxForMedia(selectedMedia, media);
this.lightboxGalleryView = new Whisper.ReactWrapperView({
className: 'lightbox-wrapper',
Component: window.Signal.Components.Lightbox,
props: {
media,
onSave: saveAttachment,
selectedIndex,
},
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
});
window.Signal.Backbone.Views.Lightbox.show(
this.lightboxGalleryView.el
);
break; break;
} }
@ -2947,12 +2946,25 @@ Whisper.ConversationView = Whisper.View.extend({
const { path, contentType } = tempAttachment; const { path, contentType } = tempAttachment;
return { return {
objectURL: getAbsoluteTempPath(path), media: [
contentType, {
onSave: null, // important so download button is omitted attachment: tempAttachment,
objectURL: getAbsoluteTempPath(path),
contentType,
index: 0,
message: {
attachments: message.get('attachments'),
id: message.get('id'),
conversationId: message.get('conversationId'),
received_at: message.get('received_at'),
received_at_ms: message.get('received_at_ms'),
},
},
],
isViewOnce: true, isViewOnce: true,
}; };
}; };
this.lightboxView = new Whisper.ReactWrapperView({ this.lightboxView = new Whisper.ReactWrapperView({
className: 'lightbox-wrapper', className: 'lightbox-wrapper',
Component: window.Signal.Components.Lightbox, Component: window.Signal.Components.Lightbox,
@ -3044,8 +3056,7 @@ Whisper.ConversationView = Whisper.View.extend({
showLightboxForMedia( showLightboxForMedia(
selectedMediaItem: MediaItemType, selectedMediaItem: MediaItemType,
media: Array<MediaItemType> = [], media: Array<MediaItemType> = []
loop = false
) { ) {
const onSave = async (options: WhatIsThis = {}) => { const onSave = async (options: WhatIsThis = {}) => {
const fullPath = await window.Signal.Types.Attachment.save({ const fullPath = await window.Signal.Types.Attachment.save({
@ -3071,7 +3082,6 @@ Whisper.ConversationView = Whisper.View.extend({
Component: window.Signal.Components.Lightbox, Component: window.Signal.Components.Lightbox,
props: { props: {
getConversation: getConversationSelector(window.reduxStore.getState()), getConversation: getConversationSelector(window.reduxStore.getState()),
loop,
media, media,
onForward: this.showForwardMessageModal.bind(this), onForward: this.showForwardMessageModal.bind(this),
onSave, onSave,
@ -3127,7 +3137,13 @@ Whisper.ConversationView = Whisper.View.extend({
message: { message: {
attachments: message.get('attachments'), attachments: message.get('attachments'),
id: message.get('id'), id: message.get('id'),
conversationId: message.get('conversationId'), conversationId:
window.ConversationController.get(
window.ConversationController.ensureContactIds({
uuid: message.get('sourceUuid'),
e164: message.get('source'),
})
)?.id || message.get('conversationId'),
received_at: message.get('received_at'), received_at: message.get('received_at'),
received_at_ms: message.get('received_at_ms'), received_at_ms: message.get('received_at_ms'),
}, },
@ -3140,7 +3156,7 @@ Whisper.ConversationView = Whisper.View.extend({
const selectedMedia = const selectedMedia =
media.find(item => attachment.path === item.path) || media[0]; media.find(item => attachment.path === item.path) || media[0];
this.showLightboxForMedia(selectedMedia, media, loop); this.showLightboxForMedia(selectedMedia, media);
}, },
showContactModal(contactId: string) { showContactModal(contactId: string) {