Fixes view once videos in lightbox
This commit is contained in:
parent
425404cd6e
commit
28f5a2bd1c
6 changed files with 104 additions and 67 deletions
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue