From d5d808651a7cc88014633d1c6a15f356fc5956c0 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Mon, 23 Aug 2021 19:14:53 -0400 Subject: [PATCH] Improved Lightbox experience --- js/modules/signal.js | 6 - stylesheets/_lightbox.scss | 76 -- stylesheets/components/Lightbox.scss | 273 ++++++ stylesheets/components/Preferences.scss | 6 +- stylesheets/manifest.scss | 2 +- ts/components/AvatarLightbox.tsx | 8 +- ts/components/Lightbox.stories.tsx | 326 +++++-- ts/components/Lightbox.tsx | 895 ++++++++---------- ts/components/LightboxGallery.stories.tsx | 93 -- ts/components/LightboxGallery.tsx | 112 --- .../ConversationDetails.tsx | 2 +- .../ConversationDetailsMediaList.stories.tsx | 2 +- .../ConversationDetailsMediaList.tsx | 2 +- .../AttachmentSection.stories.tsx | 3 +- .../media-gallery/AttachmentSection.tsx | 2 +- .../media-gallery/MediaGallery.tsx | 2 +- .../media-gallery/MediaGridItem.stories.tsx | 11 +- .../media-gallery/MediaGridItem.tsx | 2 +- .../media-gallery/groupMediaItemsByDate.ts | 2 +- ts/state/ducks/conversations.ts | 2 +- ts/state/smart/ConversationDetails.tsx | 2 +- .../media-gallery/groupMessagesByDate_test.ts | 13 +- ts/types/MediaItem.ts | 25 + ts/util/lint/exceptions.json | 41 +- ts/views/conversation_view.ts | 110 +-- ts/window.d.ts | 2 - 26 files changed, 1054 insertions(+), 966 deletions(-) delete mode 100644 stylesheets/_lightbox.scss create mode 100644 stylesheets/components/Lightbox.scss delete mode 100644 ts/components/LightboxGallery.stories.tsx delete mode 100644 ts/components/LightboxGallery.tsx create mode 100644 ts/types/MediaItem.ts diff --git a/js/modules/signal.js b/js/modules/signal.js index c93051158..a08452f83 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -41,7 +41,6 @@ const { const { Emojify } = require('../../ts/components/conversation/Emojify'); const { ErrorModal } = require('../../ts/components/ErrorModal'); const { Lightbox } = require('../../ts/components/Lightbox'); -const { LightboxGallery } = require('../../ts/components/LightboxGallery'); const { MediaGallery, } = require('../../ts/components/conversation/media-gallery/MediaGallery'); @@ -140,7 +139,6 @@ const VisualAttachment = require('./types/visual_attachment'); const EmbeddedContact = require('../../ts/types/EmbeddedContact'); const Conversation = require('./types/conversation'); const Errors = require('../../ts/types/errors'); -const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message'); const MessageType = require('./types/message'); const MIME = require('../../ts/types/MIME'); const SettingsType = require('../../ts/types/Settings'); @@ -349,7 +347,6 @@ exports.setup = (options = {}) => { Emojify, ErrorModal, Lightbox, - LightboxGallery, MediaGallery, MessageDetail, Quote, @@ -357,9 +354,6 @@ exports.setup = (options = {}) => { StagedLinkPreview, DisappearingTimeDialog, SystemTraySettingsCheckboxes, - Types: { - Message: MediaGalleryMessage, - }, WhatsNew, }; diff --git a/stylesheets/_lightbox.scss b/stylesheets/_lightbox.scss deleted file mode 100644 index c1579037d..000000000 --- a/stylesheets/_lightbox.scss +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2016-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -.lightbox-container { - display: none; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 100; -} - -.iconButton { - @include button-reset; - - // NOTE: Cannot move these to inline styles as hover breaks due to precedence. - // We use vanilla CSS-in-JS which outputs inline styles. The `:hover` - // pseudo-class cannot be expressed using vanilla CSS-in-JS, so we define it - // here. If we move the other properties to JS, they have higher precedence - // as they are inline and the `:hover` `background` change won’t override the - // base `background` definition. Revisit this as we adopt a more sophisticated - // style system in the future: - background: transparent; - width: 50px; - height: 50px; - - display: inline-block; - border-radius: 50%; - padding: 3px; - - &:before { - content: ''; - display: block; - width: 100%; - height: 100%; - } - - &:hover, - &:focus { - background: $color-gray-60; - } - - &.save { - &:before { - @include color-svg( - '../images/icons/v2/save-outline-24.svg', - $color-white - ); - } - } - - &.close { - &:before { - @include color-svg('../images/icons/v2/x-24.svg', $color-white); - } - } - - &.previous { - &:before { - @include color-svg( - '../images/icons/v2/chevron-left-24.svg', - $color-white - ); - } - } - - &.next { - &:before { - @include color-svg( - '../images/icons/v2/chevron-right-24.svg', - $color-white - ); - } - } -} diff --git a/stylesheets/components/Lightbox.scss b/stylesheets/components/Lightbox.scss new file mode 100644 index 000000000..7ed343611 --- /dev/null +++ b/stylesheets/components/Lightbox.scss @@ -0,0 +1,273 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.Lightbox { + &__container { + background-color: $color-black-alpha-80; + bottom: 0; + display: flex; + flex-direction: column; + left: 0; + padding: 0 16px; + position: absolute; + right: 0; + top: 0; + z-index: 10; + } + + &__main-container { + display: flex; + flex-direction: column; + flex-grow: 1; + // To ensure that a large image doesnt overflow the flex layout + min-height: 50px; + outline: none; + } + + &__footer { + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + min-height: 56px; + } + + &__thumbnails { + align-items: center; + display: flex; + justify-content: center; + left: 50%; + position: absolute; + + &--container { + height: 64px; + margin-bottom: 16px; + margin-top: 10px; + position: relative; + } + } + + &__thumbnail { + @include button-reset; + border-radius: 4px; + height: 64px; + margin-right: 8px; + overflow: hidden; + width: 64px; + + img { + height: 100%; + object-fit: contain; + width: 100%; + } + + &--selected { + box-shadow: 0px 0px 0px 2px $color-ultramarine; + } + + &--unavailable { + @include color-svg('../images/image.svg', $color-gray-25); + height: 100%; + width: 100%; + } + } + + &__object { + &--container { + display: inline-flex; + flex-grow: 1; + justify-content: center; + margin: 0 40px; + overflow: hidden; + position: relative; + + &--zoomed { + margin: 0; + } + } + + height: auto; + left: 50%; + max-height: 100%; + max-width: 100%; + object-fit: contain; + outline: none; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: auto; + } + + &__unsupported { + @include button-reset; + flex-grow: 1; + height: 100%; + max-width: 200px; + width: 100%; + + &--image { + @include color-svg('../images/image.svg', $color-gray-25); + } + + &--video { + @include color-svg('../images/movie.svg', $color-gray-25); + } + + &--file { + @include color-svg('../images/file.svg', $color-gray-25); + } + + &--missing { + @include color-svg( + '../images/full-screen-flow/alert-outline.svg', + $color-gray-25 + ); + } + } + + &__zoom-button { + @include button-reset; + cursor: zoom-in; + } + + &__object--container--zoomed { + .Lightbox__zoom-button { + cursor: zoom-out; + } + } + + &__caption { + @include font-body-2; + color: $color-white; + margin: 12px 0; + text-align: center; + } + + &__countdown { + padding: 8px; + } + + &__timestamp { + @include font-body-1; + background-color: $color-black; + border-radius: 15px; + color: #eeefef; + padding: 6px 18px; + text-align: center; + } + + &__nav-next { + bottom: 50%; + position: absolute; + right: 21px; + } + + &__nav-prev { + bottom: 50%; + left: 21px; + position: absolute; + } + + &__header { + align-items: center; + display: flex; + height: 56px; + justify-content: space-between; + margin-top: 24px; + + &--container { + display: flex; + } + + &--avatar { + margin-right: 12px; + } + + &--name { + @include font-body-2-bold; + color: $color-white; + } + + &--timestamp { + @include font-caption; + color: $color-gray-25; + } + } + + &__button { + @include button-reset; + + border-radius: 4px; + display: inline-block; + margin-left: 24px; + height: 24px; + width: 24px; + + &::before { + content: ''; + display: block; + height: 100%; + width: 100%; + } + + &:hover { + &::before { + background: $color-white; + } + } + + &:focus { + outline: 4px solid $color-ultramarine; + } + + &:disabled { + &::before { + background: $color-gray-65; + } + } + + &--forward { + &::before { + @include color-svg( + '../images/icons/v2/reply-solid-24.svg', + $color-gray-15 + ); + } + } + + &--save { + &::before { + @include color-svg( + '../images/icons/v2/save-solid-24.svg', + $color-gray-15 + ); + } + } + + &--close { + &::before { + @include color-svg('../images/icons/v2/x-24.svg', $color-gray-15); + } + } + + &--previous { + margin-left: 0; + &::before { + @include color-svg( + '../images/icons/v2/chevron-left-24.svg', + $color-gray-15 + ); + } + } + + &--next { + margin-left: 0; + &::before { + @include color-svg( + '../images/icons/v2/chevron-right-24.svg', + $color-gray-15 + ); + } + } + } +} diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss index fb8ab9b1c..566f01692 100644 --- a/stylesheets/components/Preferences.scss +++ b/stylesheets/components/Preferences.scss @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only @mixin preferences-icon($light_svg, $dark_svg) { - &:before { + &::before { @include light-theme { @include color-svg($light_svg, $color-gray-75); } @@ -55,7 +55,7 @@ } } - &:before { + &::before { content: ''; display: block; height: 22px; @@ -104,7 +104,7 @@ '../images/icons/v2/lock-outline-24.svg', '../images/icons/v2/lock-solid-24.svg' ); - &:before { + &::before { -webkit-mask-size: 75%; } } diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 2ae1f3154..72ef89702 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -11,7 +11,6 @@ @import 'progress'; @import 'modal'; @import 'debugLog'; -@import 'lightbox'; @import 'recorder'; @import 'emoji'; @import 'settings'; @@ -63,6 +62,7 @@ @import './components/IncomingCallBar.scss'; @import './components/Input.scss'; @import './components/LeftPaneDialog.scss'; +@import './components/Lightbox.scss'; @import './components/MediaQualitySelector.scss'; @import './components/MessageAudio.scss'; @import './components/MessageDetail.scss'; diff --git a/ts/components/AvatarLightbox.tsx b/ts/components/AvatarLightbox.tsx index 5fac66698..68ec65022 100644 --- a/ts/components/AvatarLightbox.tsx +++ b/ts/components/AvatarLightbox.tsx @@ -26,13 +26,7 @@ export const AvatarLightbox = ({ onClose, }: PropsType): JSX.Element => { return ( - + = {}): Props => ({ - caption: text('caption', overrideProps.caption || ''), +type OverridePropsMediaItemType = Partial & { caption?: string }; + +function createMediaItem( + overrideProps: OverridePropsMediaItemType +): MediaItemType { + return { + attachment: { + caption: overrideProps.caption || '', + contentType: IMAGE_JPEG, + fileName: overrideProps.objectURL, + url: overrideProps.objectURL, + }, + contentType: IMAGE_JPEG, + index: 0, + message: { + attachments: [], + conversationId: '1234', + id: 'image-msg', + received_at: 0, + received_at_ms: Date.now(), + }, + objectURL: '', + ...overrideProps, + }; +} + +const createProps = (overrideProps: Partial = {}): PropsType => ({ close: action('close'), - contentType: overrideProps.contentType || IMAGE_JPEG, i18n, - isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false), - objectURL: text('objectURL', overrideProps.objectURL || ''), - onNext: overrideProps.onNext, - onPrevious: overrideProps.onPrevious, - onSave: overrideProps.onSave, + media: overrideProps.media || [], + onSave: action('onSave'), + selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0), }); -story.add('Image', () => { +story.add('Multimedia', () => { const props = createProps({ - objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg', + media: [ + { + attachment: { + contentType: IMAGE_JPEG, + fileName: 'tina-rolf-269345-unsplash.jpg', + url: '/fixtures/tina-rolf-269345-unsplash.jpg', + caption: + 'Still from The Lighthouse, starring Robert Pattinson and Willem Defoe.', + }, + contentType: IMAGE_JPEG, + index: 0, + message: { + attachments: [], + conversationId: '1234', + id: 'image-msg', + received_at: 1, + received_at_ms: Date.now(), + }, + objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg', + }, + { + attachment: { + contentType: VIDEO_MP4, + fileName: 'pixabay-Soap-Bubble-7141.mp4', + url: '/fixtures/pixabay-Soap-Bubble-7141.mp4', + }, + contentType: VIDEO_MP4, + index: 1, + message: { + attachments: [], + conversationId: '1234', + id: 'video-msg', + received_at: 2, + received_at_ms: Date.now(), + }, + objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4', + }, + createMediaItem({ + contentType: IMAGE_JPEG, + index: 2, + thumbnailObjectUrl: '/fixtures/kitten-1-64-64.jpg', + objectURL: '/fixtures/kitten-1-64-64.jpg', + }), + createMediaItem({ + contentType: IMAGE_JPEG, + index: 3, + thumbnailObjectUrl: '/fixtures/kitten-2-64-64.jpg', + objectURL: '/fixtures/kitten-2-64-64.jpg', + }), + ], }); return ; }); -story.add('Image with Caption (normal image)', () => { +story.add('Missing Media', () => { const props = createProps({ - caption: - 'This is the user-provided caption. It can get long and wrap onto multiple lines.', - objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg', + media: [ + { + attachment: { + contentType: IMAGE_JPEG, + fileName: 'tina-rolf-269345-unsplash.jpg', + url: '/fixtures/tina-rolf-269345-unsplash.jpg', + }, + contentType: IMAGE_JPEG, + index: 0, + message: { + attachments: [], + conversationId: '1234', + id: 'image-msg', + received_at: 3, + received_at_ms: Date.now(), + }, + objectURL: undefined, + }, + ], }); return ; }); -story.add('Image with Caption (all-white image)', () => { - const props = createProps({ - caption: - 'This is the user-provided caption. It should be visible on light backgrounds.', - objectURL: '/fixtures/2000x2000-white.png', - }); +story.add('Single Image', () => ( + +)); - return ; -}); +story.add('Image with Caption (normal image)', () => ( + +)); -story.add('Video', () => { - const props = createProps({ - contentType: VIDEO_MP4, - objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - }); +story.add('Image with Caption (all-white image)', () => ( + +)); - return ; -}); +story.add('Single Video', () => ( + +)); -story.add('Video with Caption', () => { - const props = createProps({ - caption: - 'This is the user-provided caption. It can get long and wrap onto multiple lines.', - contentType: VIDEO_MP4, - objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - }); +story.add('Single Video w/caption', () => ( + +)); - return ; -}); +story.add('Unsupported Image Type', () => ( + +)); -story.add('Video (View Once)', () => { - const props = createProps({ - contentType: VIDEO_MP4, - isViewOnce: true, - objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - }); +story.add('Unsupported Video Type', () => ( + +)); - return ; -}); - -story.add('Unsupported Image Type', () => { - const props = createProps({ - contentType: stringToMIMEType('image/tiff'), - objectURL: 'unsupported-image.tiff', - }); - - return ; -}); - -story.add('Unsupported Video Type', () => { - const props = createProps({ - contentType: VIDEO_QUICKTIME, - objectURL: 'unsupported-video.mov', - }); - - return ; -}); - -story.add('Unsupported ContentType', () => { - const props = createProps({ - contentType: AUDIO_MP3, - objectURL: '/fixtures/incompetech-com-Agnus-Dei-X.mp3', - }); - - return ; -}); - -story.add('Including Next/Previous/Save Callbacks', () => { - const props = createProps({ - objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg', - onNext: action('onNext'), - onPrevious: action('onPrevious'), - onSave: action('onSave'), - }); - - return ; -}); +story.add('Unsupported Content', () => ( + +)); story.add('Custom children', () => ( - +
(
)); + +story.add('Forwarding', () => ( + +)); + +story.add('Conversation Header', () => ( + ({ + acceptedMessageRequest: true, + avatarPath: '/fixtures/kitten-1-64-64.jpg', + id: '1234', + isMe: false, + name: 'Test', + profileName: 'Test', + sharedGroupNames: [], + title: 'Test', + type: 'direct', + })} + media={[ + createMediaItem({ + contentType: VIDEO_MP4, + objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4', + }), + ]} + /> +)); diff --git a/ts/components/Lightbox.tsx b/ts/components/Lightbox.tsx index ef1f1a89c..0054440ca 100644 --- a/ts/components/Lightbox.tsx +++ b/ts/components/Lightbox.tsx @@ -1,291 +1,137 @@ -// Copyright 2018-2020 Signal Messenger, LLC +// Copyright 2018-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { ReactNode } from 'react'; - +import React, { + MouseEvent, + ReactNode, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import moment from 'moment'; import classNames from 'classnames'; -import is from '@sindresorhus/is'; +import { createPortal } from 'react-dom'; import * as GoogleChrome from '../util/GoogleChrome'; -import * as MIME from '../types/MIME'; - -import { formatDuration } from '../util/formatDuration'; +import { AttachmentType, isGIF } from '../types/Attachment'; +import { Avatar, AvatarSize } from './Avatar'; +import { ConversationType } from '../state/ducks/conversations'; +import { IMAGE_PNG, isImage, isVideo } from '../types/MIME'; import { LocalizerType } from '../types/Util'; +import { MediaItemType, MessageAttributesType } from '../types/MediaItem'; -const Colors = { - ICON_SECONDARY: '#b9b9b9', -}; - -const colorSVG = (url: string, color: string) => { - return { - WebkitMask: `url(${url}) no-repeat center`, - WebkitMaskSize: '100%', - backgroundColor: color, - }; -}; - -export type Props = { +export type PropsType = { children?: ReactNode; close: () => void; - contentType: MIME.MIMEType | undefined; + getConversation?: (id: string) => ConversationType; i18n: LocalizerType; - objectURL: string; - caption?: string; - isViewOnce: boolean; - loop?: boolean; - onNext?: () => void; - onPrevious?: () => void; - onSave?: () => void; -}; -type State = { - videoTime?: number; + media: Array; + onForward?: (messageId: string) => void; + onSave?: (options: { + attachment: AttachmentType; + message: MessageAttributesType; + index: number; + }) => void; + selectedIndex?: number; }; -const CONTROLS_WIDTH = 50; -const CONTROLS_SPACING = 10; +export function Lightbox({ + children, + close, + getConversation, + media, + i18n, + onForward, + onSave, + selectedIndex: initialSelectedIndex, +}: PropsType): JSX.Element | null { + const [root, setRoot] = React.useState(); + const [selectedIndex, setSelectedIndex] = useState( + initialSelectedIndex || 0 + ); -const styles = { - container: { - display: 'flex', - flexDirection: 'column', - position: 'absolute', - left: 0, - right: 0, - top: 0, - bottom: 0, - backgroundColor: 'rgba(0, 0, 0, 0.9)', - zIndex: 10, - } as React.CSSProperties, - buttonContainer: { - backgroundColor: 'transparent', - border: 'none', - display: 'flex', - flexDirection: 'column', - outline: 'none', - width: '100%', - padding: 0, - } as React.CSSProperties, - mainContainer: { - display: 'flex', - flexDirection: 'row', - flexGrow: 1, - paddingTop: 40, - paddingLeft: 40, - paddingRight: 40, - paddingBottom: 0, - // To ensure that a large image doesn't overflow the flex layout - minHeight: '50px', - outline: 'none', - } as React.CSSProperties, - objectContainer: { - position: 'relative', - flexGrow: 1, - display: 'inline-flex', - justifyContent: 'center', - } as React.CSSProperties, - object: { - flexGrow: 1, - flexShrink: 1, - maxWidth: '100%', - maxHeight: '100%', - objectFit: 'contain', - outline: 'none', - } as React.CSSProperties, - img: { - position: 'absolute', - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)', - width: 'auto', - height: 'auto', - maxWidth: '100%', - maxHeight: '100%', - objectFit: 'contain', - outline: 'none', - } as React.CSSProperties, - caption: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - textAlign: 'center', - color: 'white', - fontWeight: 'bold', - textShadow: '0 0 1px black, 0 0 2px black, 0 0 3px black, 0 0 4px black', - padding: '1em', - paddingLeft: '3em', - paddingRight: '3em', - backgroundColor: 'rgba(192, 192, 192, .20)', - } as React.CSSProperties, - controlsOffsetPlaceholder: { - width: CONTROLS_WIDTH, - marginRight: CONTROLS_SPACING, - flexShrink: 0, - }, - controls: { - width: CONTROLS_WIDTH, - flexShrink: 0, - display: 'flex', - flexDirection: 'column', - marginLeft: CONTROLS_SPACING, - } as React.CSSProperties, - navigationContainer: { - flexShrink: 0, - display: 'flex', - flexDirection: 'row', - justifyContent: 'center', - padding: 10, - } as React.CSSProperties, - saveButton: { - marginTop: 10, - }, - countdownContainer: { - padding: 8, - }, - iconButtonPlaceholder: { - // Dimensions match `.iconButton`: - display: 'inline-block', - width: 50, - height: 50, - }, - timestampPill: { - borderRadius: '15px', - backgroundColor: '#000000', - color: '#eeefef', - fontSize: '16px', - letterSpacing: '0px', - lineHeight: '18px', - // This cast is necessary or typescript chokes - textAlign: 'center' as const, - padding: '6px', - paddingLeft: '18px', - paddingRight: '18px', - }, -}; + const [previousFocus, setPreviousFocus] = useState(); + const [zoomed, setZoomed] = useState(false); + const containerRef = useRef(null); + const focusRef = useRef(null); + const videoRef = useRef(null); -type IconButtonProps = { - i18n: LocalizerType; - onClick?: () => void; - style?: React.CSSProperties; - type: 'save' | 'close' | 'previous' | 'next'; -}; - -const IconButton = ({ i18n, onClick, style, type }: IconButtonProps) => { - const clickHandler = (event: React.MouseEvent): void => { - event.preventDefault(); - if (!onClick) { - return; + const restorePreviousFocus = useCallback(() => { + if (previousFocus && previousFocus.focus) { + previousFocus.focus(); } + }, [previousFocus]); - onClick(); + const onPrevious = useCallback(() => { + setSelectedIndex(prevSelectedIndex => Math.max(prevSelectedIndex - 1, 0)); + }, []); + + const onNext = useCallback(() => { + setSelectedIndex(prevSelectedIndex => + Math.min(prevSelectedIndex + 1, media.length - 1) + ); + }, [media]); + + const handleSave = () => { + const mediaItem = media[selectedIndex]; + const { attachment, message, index } = mediaItem; + + onSave?.({ attachment, message, index }); }; - return ( - - ); + useEffect(() => { + if (!previousFocus) { + setPreviousFocus(document.activeElement as HTMLElement); } + }, [previousFocus]); + useEffect(() => { + return () => { + restorePreviousFocus(); + }; + }, [restorePreviousFocus]); + + useEffect(() => { + const useCapture = true; + document.addEventListener('keydown', onKeyDown, useCapture); + + return () => { + document.removeEventListener('keydown', onKeyDown, useCapture); + }; + }, [onKeyDown]); + + useEffect(() => { + // Wait until we're added to the DOM. ConversationView first creates this + // view, then appends its elements into the DOM. + const timeout = window.setTimeout(() => { + playVideo(); + + if (focusRef && focusRef.current) { + focusRef.current.focus(); + } + }); + + return () => { + if (timeout) { + window.clearTimeout(timeout); + } + }; + }, [selectedIndex]); + + const { attachment, contentType, loop = false, objectURL, message } = + media[selectedIndex] || {}; + const caption = attachment?.caption; + + let content: JSX.Element; + if (!contentType) { + content = <>{children}; + } else { + const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType); const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType); - if (isVideoTypeSupported) { - return ( + const isUnsupportedImageType = + !isImageTypeSupported && isImage(contentType); + const isUnsupportedVideoType = + !isVideoTypeSupported && isVideo(contentType); + + if (isImageTypeSupported) { + if (objectURL) { + content = ( + + ); + } else { + content = ( + + ))} + + + )} + + )} + , + root + ) + : null; +} + +function LightboxHeader({ + getConversation, + i18n, + message, +}: { + getConversation: (id: string) => ConversationType; + i18n: LocalizerType; + message: MessageAttributesType; +}): JSX.Element { + const conversation = getConversation(message.conversationId); + + return ( +
+
+ +
+
+
{conversation.title}
+
+ {moment(message.received_at_ms).format('L LT')} +
+
+
+ ); } diff --git a/ts/components/LightboxGallery.stories.tsx b/ts/components/LightboxGallery.stories.tsx deleted file mode 100644 index 568a84461..000000000 --- a/ts/components/LightboxGallery.stories.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import * as React from 'react'; - -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; -import { number } from '@storybook/addon-knobs'; - -import { LightboxGallery, Props } from './LightboxGallery'; -import { setup as setupI18n } from '../../js/modules/i18n'; -import enMessages from '../../_locales/en/messages.json'; -import { IMAGE_JPEG, VIDEO_MP4 } from '../types/MIME'; - -const i18n = setupI18n('en', enMessages); - -const story = storiesOf('Components/LightboxGallery', module); - -const createProps = (overrideProps: Partial = {}): Props => ({ - close: action('close'), - i18n, - media: overrideProps.media || [], - onSave: action('onSave'), - selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0), -}); - -story.add('Image and Video', () => { - const props = createProps({ - media: [ - { - attachment: { - contentType: IMAGE_JPEG, - fileName: 'tina-rolf-269345-unsplash.jpg', - url: '/fixtures/tina-rolf-269345-unsplash.jpg', - caption: - 'Still from The Lighthouse, starring Robert Pattinson and Willem Defoe.', - }, - contentType: IMAGE_JPEG, - index: 0, - message: { - attachments: [], - id: 'image-msg', - received_at: 1, - received_at_ms: Date.now(), - }, - objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg', - }, - { - attachment: { - contentType: VIDEO_MP4, - fileName: 'pixabay-Soap-Bubble-7141.mp4', - url: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - }, - contentType: VIDEO_MP4, - index: 1, - message: { - attachments: [], - id: 'video-msg', - received_at: 2, - received_at_ms: Date.now(), - }, - objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4', - }, - ], - }); - - return ; -}); - -story.add('Missing Media', () => { - const props = createProps({ - media: [ - { - attachment: { - contentType: IMAGE_JPEG, - fileName: 'tina-rolf-269345-unsplash.jpg', - url: '/fixtures/tina-rolf-269345-unsplash.jpg', - }, - contentType: IMAGE_JPEG, - index: 0, - message: { - attachments: [], - id: 'image-msg', - received_at: 3, - received_at_ms: Date.now(), - }, - objectURL: undefined, - }, - ], - }); - - return ; -}); diff --git a/ts/components/LightboxGallery.tsx b/ts/components/LightboxGallery.tsx deleted file mode 100644 index 03a1d8b7d..000000000 --- a/ts/components/LightboxGallery.tsx +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2018-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; - -import * as MIME from '../types/MIME'; -import { Lightbox } from './Lightbox'; -import { Message } from './conversation/media-gallery/types/Message'; - -import { AttachmentType } from '../types/Attachment'; -import { LocalizerType } from '../types/Util'; - -export type MediaItemType = { - objectURL?: string; - thumbnailObjectUrl?: string; - contentType?: MIME.MIMEType; - index: number; - attachment: AttachmentType; - message: Message; -}; - -export type Props = { - close: () => void; - i18n: LocalizerType; - media: Array; - onSave?: (options: { - attachment: AttachmentType; - message: Message; - index: number; - }) => void; - selectedIndex: number; -}; - -type State = { - selectedIndex: number; -}; - -export class LightboxGallery extends React.Component { - public static defaultProps: Partial = { - selectedIndex: 0, - }; - - constructor(props: Props) { - super(props); - - this.state = { - selectedIndex: props.selectedIndex, - }; - } - - public render(): JSX.Element { - const { close, media, onSave, i18n } = this.props; - const { selectedIndex } = this.state; - - const selectedMedia = media[selectedIndex]; - const firstIndex = 0; - const lastIndex = media.length - 1; - - const onPrevious = - selectedIndex > firstIndex ? this.handlePrevious : undefined; - const onNext = selectedIndex < lastIndex ? this.handleNext : undefined; - - const objectURL = - selectedMedia.objectURL || 'images/full-screen-flow/alert-outline.svg'; - const { attachment } = selectedMedia; - - const saveCallback = onSave ? this.handleSave : undefined; - const captionCallback = attachment ? attachment.caption : undefined; - - return ( - - ); - } - - private readonly handlePrevious = () => { - this.setState(prevState => ({ - selectedIndex: Math.max(prevState.selectedIndex - 1, 0), - })); - }; - - private readonly handleNext = () => { - this.setState((prevState, props) => ({ - selectedIndex: Math.min( - prevState.selectedIndex + 1, - props.media.length - 1 - ), - })); - }; - - private readonly handleSave = () => { - const { media, onSave } = this.props; - if (!onSave) { - return; - } - - const { selectedIndex } = this.state; - const mediaItem = media[selectedIndex]; - const { attachment, message, index } = mediaItem; - - onSave({ attachment, message, index }); - }; -} diff --git a/ts/components/conversation/conversation-details/ConversationDetails.tsx b/ts/components/conversation/conversation-details/ConversationDetails.tsx index 9455bf7a2..d77d284ed 100644 --- a/ts/components/conversation/conversation-details/ConversationDetails.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetails.tsx @@ -8,7 +8,7 @@ import { assert } from '../../../util/assert'; import { getMutedUntilText } from '../../../util/getMutedUntilText'; import { LocalizerType } from '../../../types/Util'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { missingCaseError } from '../../../util/missingCaseError'; import { DisappearingTimerSelect } from '../../DisappearingTimerSelect'; diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx index 964cadeb9..d93253ed5 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.stories.tsx @@ -17,7 +17,7 @@ import { createPreparedMediaItems, createRandomMedia, } from '../media-gallery/AttachmentSection.stories'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation'; const i18n = setupI18n('en', enMessages); diff --git a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx index fb04346e5..c13232053 100644 --- a/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx +++ b/ts/components/conversation/conversation-details/ConversationDetailsMediaList.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { LocalizerType } from '../../../types/Util'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { ConversationType } from '../../../state/ducks/conversations'; import { PanelSection } from './PanelSection'; diff --git a/ts/components/conversation/media-gallery/AttachmentSection.stories.tsx b/ts/components/conversation/media-gallery/AttachmentSection.stories.tsx index 4bde261e7..ef5ea6243 100644 --- a/ts/components/conversation/media-gallery/AttachmentSection.stories.tsx +++ b/ts/components/conversation/media-gallery/AttachmentSection.stories.tsx @@ -11,7 +11,7 @@ import { random, range, sample, sortBy } from 'lodash'; import { setup as setupI18n } from '../../../../js/modules/i18n'; import enMessages from '../../../../_locales/en/messages.json'; import { MIMEType } from '../../../types/MIME'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { AttachmentSection, Props } from './AttachmentSection'; @@ -51,6 +51,7 @@ const createRandomFile = ( return { contentType, message: { + conversationId: '123', id: random(now).toString(), received_at: Math.floor(Math.random() * 10), received_at_ms: random(startTime, startTime + timeWindow), diff --git a/ts/components/conversation/media-gallery/AttachmentSection.tsx b/ts/components/conversation/media-gallery/AttachmentSection.tsx index 978d26dc7..1e924c5b1 100644 --- a/ts/components/conversation/media-gallery/AttachmentSection.tsx +++ b/ts/components/conversation/media-gallery/AttachmentSection.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { DocumentListItem } from './DocumentListItem'; import { ItemClickEvent } from './types/ItemClickEvent'; import { MediaGridItem } from './MediaGridItem'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { missingCaseError } from '../../../util/missingCaseError'; import { LocalizerType } from '../../../types/Util'; import { getMessageTimestamp } from '../../../util/getMessageTimestamp'; diff --git a/ts/components/conversation/media-gallery/MediaGallery.tsx b/ts/components/conversation/media-gallery/MediaGallery.tsx index 3be5abf2f..c7275963e 100644 --- a/ts/components/conversation/media-gallery/MediaGallery.tsx +++ b/ts/components/conversation/media-gallery/MediaGallery.tsx @@ -14,7 +14,7 @@ import { missingCaseError } from '../../../util/missingCaseError'; import { LocalizerType } from '../../../types/Util'; import { getMessageTimestamp } from '../../../util/getMessageTimestamp'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; export type Props = { documents: Array; diff --git a/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx b/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx index bf74afb01..276eb1e96 100644 --- a/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx +++ b/ts/components/conversation/media-gallery/MediaGridItem.stories.tsx @@ -8,12 +8,11 @@ import { action } from '@storybook/addon-actions'; import { setup as setupI18n } from '../../../../js/modules/i18n'; import enMessages from '../../../../_locales/en/messages.json'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { AttachmentType } from '../../../types/Attachment'; import { stringToMIMEType } from '../../../types/MIME'; import { MediaGridItem, Props } from './MediaGridItem'; -import { Message } from './types/Message'; const i18n = setupI18n('en', enMessages); @@ -45,7 +44,13 @@ const createMediaItem = ( ), index: 0, attachment: {} as AttachmentType, // attachment not useful in the component - message: {} as Message, // message not used in the component + message: { + attachments: [], + conversationId: '1234', + id: 'id', + received_at: Date.now(), + received_at_ms: Date.now(), + }, }); story.add('Image', () => { diff --git a/ts/components/conversation/media-gallery/MediaGridItem.tsx b/ts/components/conversation/media-gallery/MediaGridItem.tsx index 59f97a9ea..f115d749f 100644 --- a/ts/components/conversation/media-gallery/MediaGridItem.tsx +++ b/ts/components/conversation/media-gallery/MediaGridItem.tsx @@ -9,7 +9,7 @@ import { isVideoTypeSupported, } from '../../../util/GoogleChrome'; import { LocalizerType } from '../../../types/Util'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; export type Props = { mediaItem: MediaItemType; diff --git a/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts b/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts index d63e34be5..214a5f863 100644 --- a/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts +++ b/ts/components/conversation/media-gallery/groupMediaItemsByDate.ts @@ -4,7 +4,7 @@ import moment from 'moment'; import { compact, groupBy, sortBy } from 'lodash'; -import { MediaItemType } from '../../LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; import { getMessageTimestamp } from '../../../util/getMessageTimestamp'; // import { missingCaseError } from '../../../util/missingCaseError'; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 054d919c1..533cc0ed9 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -42,7 +42,7 @@ import { } from '../../model-types.d'; import { BodyRangeType } from '../../types/Util'; import { CallMode } from '../../types/Calling'; -import { MediaItemType } from '../../components/LightboxGallery'; +import { MediaItemType } from '../../types/MediaItem'; import { getGroupSizeRecommendedLimit, getGroupSizeHardLimit, diff --git a/ts/state/smart/ConversationDetails.tsx b/ts/state/smart/ConversationDetails.tsx index 65a3c785c..856bb3660 100644 --- a/ts/state/smart/ConversationDetails.tsx +++ b/ts/state/smart/ConversationDetails.tsx @@ -15,7 +15,7 @@ import { } from '../selectors/conversations'; import { getGroupMemberships } from '../../util/getGroupMemberships'; import { getIntl } from '../selectors/user'; -import { MediaItemType } from '../../components/LightboxGallery'; +import { MediaItemType } from '../../types/MediaItem'; import { assert } from '../../util/assert'; import { SignalService as Proto } from '../../protobuf'; diff --git a/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts b/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts index 9b6af88f5..cc7483015 100644 --- a/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts +++ b/ts/test-node/components/media-gallery/groupMessagesByDate_test.ts @@ -9,12 +9,13 @@ import { groupMediaItemsByDate, Section, } from '../../../components/conversation/media-gallery/groupMediaItemsByDate'; -import { MediaItemType } from '../../../components/LightboxGallery'; +import { MediaItemType } from '../../../types/MediaItem'; const toMediaItem = (date: Date): MediaItemType => ({ objectURL: date.toUTCString(), index: 0, message: { + conversationId: '1234', id: 'id', received_at: date.getTime(), received_at_ms: date.getTime(), @@ -56,6 +57,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Thu, 12 Apr 2018 12:00:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1523534400000, received_at_ms: 1523534400000, @@ -71,6 +73,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1523491260000, received_at_ms: 1523491260000, @@ -91,6 +94,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Wed, 11 Apr 2018 23:59:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1523491140000, received_at_ms: 1523491140000, @@ -111,6 +115,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Mon, 09 Apr 2018 00:01:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1523232060000, received_at_ms: 1523232060000, @@ -131,6 +136,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Sun, 08 Apr 2018 23:59:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1523231940000, received_at_ms: 1523231940000, @@ -146,6 +152,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1522540860000, received_at_ms: 1522540860000, @@ -168,6 +175,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Sat, 31 Mar 2018 23:59:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1522540740000, received_at_ms: 1522540740000, @@ -183,6 +191,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1519912800000, received_at_ms: 1519912800000, @@ -205,6 +214,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Mon, 28 Feb 2011 23:59:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1298937540000, received_at_ms: 1298937540000, @@ -220,6 +230,7 @@ describe('groupMediaItemsByDate', () => { objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT', index: 0, message: { + conversationId: '1234', id: 'id', received_at: 1296554400000, received_at_ms: 1296554400000, diff --git a/ts/types/MediaItem.ts b/ts/types/MediaItem.ts new file mode 100644 index 000000000..9826b9303 --- /dev/null +++ b/ts/types/MediaItem.ts @@ -0,0 +1,25 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { AttachmentType } from './Attachment'; +import { MIMEType } from './MIME'; + +export type MessageAttributesType = { + attachments: Array; + conversationId: string; + id: string; + // eslint-disable-next-line camelcase + received_at: number; + // eslint-disable-next-line camelcase + received_at_ms: number; +}; + +export type MediaItemType = { + attachment: AttachmentType; + contentType?: MIMEType; + index: number; + loop?: boolean; + message: MessageAttributesType; + objectURL?: string; + thumbnailObjectUrl?: string; +}; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 33e76c78d..f73115ea5 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -13538,27 +13538,46 @@ "updated": "2020-07-21T18:34:59.251Z" }, { - "rule": "React-createRef", + "rule": "React-useRef", "path": "ts/components/Lightbox.js", - "line": " this.containerRef = react_1.default.createRef();", + "line": " const containerRef = react_1.useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2019-11-06T19:56:38.557Z", - "reasonDetail": "Used to double-check outside clicks" + "updated": "2021-08-23T18:39:37.081Z" }, { - "rule": "React-createRef", + "rule": "React-useRef", "path": "ts/components/Lightbox.js", - "line": " this.focusRef = react_1.default.createRef();", + "line": " const focusRef = react_1.useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2019-11-06T19:56:38.557Z", - "reasonDetail": "Used to manage focus" + "updated": "2021-08-23T18:39:37.081Z" }, { - "rule": "React-createRef", + "rule": "React-useRef", "path": "ts/components/Lightbox.js", - "line": " this.videoRef = react_1.default.createRef();", + "line": " const videoRef = react_1.useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2020-09-14T23:03:44.863Z" + "updated": "2021-08-23T18:39:37.081Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Lightbox.tsx", + "line": " const containerRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-08-23T18:39:37.081Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Lightbox.tsx", + "line": " const focusRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-08-23T18:39:37.081Z" + }, + { + "rule": "React-useRef", + "path": "ts/components/Lightbox.tsx", + "line": " const videoRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2021-08-23T18:39:37.081Z" }, { "rule": "React-createRef", diff --git a/ts/views/conversation_view.ts b/ts/views/conversation_view.ts index 3dc5e4ffa..8b4a5f322 100644 --- a/ts/views/conversation_view.ts +++ b/ts/views/conversation_view.ts @@ -26,7 +26,7 @@ import { MessageAttributesType, } from '../model-types.d'; import { LinkPreviewType } from '../types/message/LinkPreviews'; -import { MediaItemType } from '../components/LightboxGallery'; +import { MediaItemType } from '../types/MediaItem'; import { MessageModel } from '../models/messages'; import { assert } from '../util/assert'; import { maybeParseUrl } from '../util/url'; @@ -47,7 +47,10 @@ import { isTapToView, } from '../state/selectors/message'; import { isMessageUnread } from '../util/isMessageUnread'; -import { getMessagesByConversation } from '../state/selectors/conversations'; +import { + getConversationSelector, + getMessagesByConversation, +} from '../state/selectors/conversations'; import { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList'; import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog'; import { @@ -2654,7 +2657,7 @@ Whisper.ConversationView = Whisper.View.extend({ ); this.lightboxGalleryView = new Whisper.ReactWrapperView({ className: 'lightbox-wrapper', - Component: window.Signal.Components.LightboxGallery, + Component: window.Signal.Components.Lightbox, props: { media, onSave: saveAttachment, @@ -3039,10 +3042,10 @@ Whisper.ConversationView = Whisper.View.extend({ }); }, - // TODO: DESKTOP-1133 (DRY up these lightboxes) showLightboxForMedia( - selectedMediaItem: WhatIsThis, - media: Array = [] + selectedMediaItem: MediaItemType, + media: Array = [], + loop = false ) { const onSave = async (options: WhatIsThis = {}) => { const fullPath = await window.Signal.Types.Attachment.save({ @@ -3065,11 +3068,14 @@ Whisper.ConversationView = Whisper.View.extend({ this.lightboxGalleryView = new Whisper.ReactWrapperView({ className: 'lightbox-wrapper', - Component: window.Signal.Components.LightboxGallery, + Component: window.Signal.Components.Lightbox, props: { + getConversation: getConversationSelector(window.reduxStore.getState()), + loop, media, + onForward: this.showForwardMessageModal.bind(this), onSave, - selectedIndex, + selectedIndex: selectedIndex >= 0 ? selectedIndex : 0, }, onClose: () => window.Signal.Backbone.Views.Lightbox.hide(), }); @@ -3096,7 +3102,7 @@ Whisper.ConversationView = Whisper.View.extend({ return; } - const { contentType, path } = attachment; + const { contentType } = attachment; if ( !window.Signal.Util.GoogleChrome.isImageTypeSupported(contentType) && @@ -3118,71 +3124,23 @@ Whisper.ConversationView = Whisper.View.extend({ contentType: item.contentType, loop, index, - message, + 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'), + }, attachment: item, + thumbnailObjectUrl: + item.thumbnail?.objectUrl || + getAbsoluteAttachmentPath(item.thumbnail?.path ?? ''), })); - if (media.length === 1) { - const props = { - objectURL: getAbsoluteAttachmentPath(path ?? ''), - contentType, - caption: attachment.caption, - loop, - onSave: () => { - const timestamp = message.get('sent_at'); - this.downloadAttachment({ attachment, timestamp, message }); - }, - }; - this.lightboxView = new Whisper.ReactWrapperView({ - className: 'lightbox-wrapper', - Component: window.Signal.Components.Lightbox, - props, - onClose: () => { - window.Signal.Backbone.Views.Lightbox.hide(); - this.stopListening(message); - }, - }); - this.listenTo(message, 'expired', () => this.lightboxView.remove()); - window.Signal.Backbone.Views.Lightbox.show(this.lightboxView.el); - return; - } + const selectedMedia = + media.find(item => attachment.path === item.path) || media[0]; - const selectedIndex = window._.findIndex( - media, - item => attachment.path === item.path - ); - - const onSave = async (options: WhatIsThis = {}) => { - const fullPath = await window.Signal.Types.Attachment.save({ - attachment: options.attachment, - index: options.index + 1, - readAttachmentData, - saveAttachmentToDisk, - timestamp: options.message.get('sent_at'), - }); - - if (fullPath) { - this.showToast(Whisper.FileSavedToast, { fullPath }); - } - }; - - const props = { - media, - loop, - selectedIndex: selectedIndex >= 0 ? selectedIndex : 0, - onSave, - }; - this.lightboxGalleryView = new Whisper.ReactWrapperView({ - className: 'lightbox-wrapper', - Component: window.Signal.Components.LightboxGallery, - props, - onClose: () => { - window.Signal.Backbone.Views.Lightbox.hide(); - this.stopListening(message); - }, - }); - this.listenTo(message, 'expired', () => this.lightboxGalleryView.remove()); - window.Signal.Backbone.Views.Lightbox.show(this.lightboxGalleryView.el); + this.showLightboxForMedia(selectedMedia, media, loop); }, showContactModal(contactId: string) { @@ -3608,9 +3566,15 @@ Whisper.ConversationView = Whisper.View.extend({ contentType: attachment.contentType, index, attachment, - // this message is a valid structure, but doesn't work with ts - // eslint-disable-next-line @typescript-eslint/no-explicit-any - message: message as any, + message: { + attachments: message.attachments || [], + conversationId: + window.ConversationController.get(message.sourceUuid)?.id || + message.conversationId, + id: message.id, + received_at: message.received_at, + received_at_ms: Number(message.received_at_ms), + }, }; } ), diff --git a/ts/window.d.ts b/ts/window.d.ts index 14e544628..aaef99cf3 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -96,7 +96,6 @@ import { ContactDetail } from './components/conversation/ContactDetail'; import { ContactModal } from './components/conversation/ContactModal'; import { ErrorModal } from './components/ErrorModal'; import { Lightbox } from './components/Lightbox'; -import { LightboxGallery } from './components/LightboxGallery'; import { MediaGallery } from './components/conversation/media-gallery/MediaGallery'; import { MessageDetail } from './components/conversation/MessageDetail'; import { ProgressModal } from './components/ProgressModal'; @@ -421,7 +420,6 @@ declare global { DisappearingTimeDialog: typeof DisappearingTimeDialog; ErrorModal: typeof ErrorModal; Lightbox: typeof Lightbox; - LightboxGallery: typeof LightboxGallery; MediaGallery: typeof MediaGallery; MessageDetail: typeof MessageDetail; ProgressModal: typeof ProgressModal;