Improved Lightbox experience
This commit is contained in:
parent
d80e738fb1
commit
d5d808651a
|
@ -41,7 +41,6 @@ const {
|
||||||
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
const { Emojify } = require('../../ts/components/conversation/Emojify');
|
||||||
const { ErrorModal } = require('../../ts/components/ErrorModal');
|
const { ErrorModal } = require('../../ts/components/ErrorModal');
|
||||||
const { Lightbox } = require('../../ts/components/Lightbox');
|
const { Lightbox } = require('../../ts/components/Lightbox');
|
||||||
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
|
|
||||||
const {
|
const {
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
} = require('../../ts/components/conversation/media-gallery/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 EmbeddedContact = require('../../ts/types/EmbeddedContact');
|
||||||
const Conversation = require('./types/conversation');
|
const Conversation = require('./types/conversation');
|
||||||
const Errors = require('../../ts/types/errors');
|
const Errors = require('../../ts/types/errors');
|
||||||
const MediaGalleryMessage = require('../../ts/components/conversation/media-gallery/types/Message');
|
|
||||||
const MessageType = require('./types/message');
|
const MessageType = require('./types/message');
|
||||||
const MIME = require('../../ts/types/MIME');
|
const MIME = require('../../ts/types/MIME');
|
||||||
const SettingsType = require('../../ts/types/Settings');
|
const SettingsType = require('../../ts/types/Settings');
|
||||||
|
@ -349,7 +347,6 @@ exports.setup = (options = {}) => {
|
||||||
Emojify,
|
Emojify,
|
||||||
ErrorModal,
|
ErrorModal,
|
||||||
Lightbox,
|
Lightbox,
|
||||||
LightboxGallery,
|
|
||||||
MediaGallery,
|
MediaGallery,
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
Quote,
|
Quote,
|
||||||
|
@ -357,9 +354,6 @@ exports.setup = (options = {}) => {
|
||||||
StagedLinkPreview,
|
StagedLinkPreview,
|
||||||
DisappearingTimeDialog,
|
DisappearingTimeDialog,
|
||||||
SystemTraySettingsCheckboxes,
|
SystemTraySettingsCheckboxes,
|
||||||
Types: {
|
|
||||||
Message: MediaGalleryMessage,
|
|
||||||
},
|
|
||||||
WhatsNew,
|
WhatsNew,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
@mixin preferences-icon($light_svg, $dark_svg) {
|
@mixin preferences-icon($light_svg, $dark_svg) {
|
||||||
&:before {
|
&::before {
|
||||||
@include light-theme {
|
@include light-theme {
|
||||||
@include color-svg($light_svg, $color-gray-75);
|
@include color-svg($light_svg, $color-gray-75);
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
|
@ -104,7 +104,7 @@
|
||||||
'../images/icons/v2/lock-outline-24.svg',
|
'../images/icons/v2/lock-outline-24.svg',
|
||||||
'../images/icons/v2/lock-solid-24.svg'
|
'../images/icons/v2/lock-solid-24.svg'
|
||||||
);
|
);
|
||||||
&:before {
|
&::before {
|
||||||
-webkit-mask-size: 75%;
|
-webkit-mask-size: 75%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
@import 'progress';
|
@import 'progress';
|
||||||
@import 'modal';
|
@import 'modal';
|
||||||
@import 'debugLog';
|
@import 'debugLog';
|
||||||
@import 'lightbox';
|
|
||||||
@import 'recorder';
|
@import 'recorder';
|
||||||
@import 'emoji';
|
@import 'emoji';
|
||||||
@import 'settings';
|
@import 'settings';
|
||||||
|
@ -63,6 +62,7 @@
|
||||||
@import './components/IncomingCallBar.scss';
|
@import './components/IncomingCallBar.scss';
|
||||||
@import './components/Input.scss';
|
@import './components/Input.scss';
|
||||||
@import './components/LeftPaneDialog.scss';
|
@import './components/LeftPaneDialog.scss';
|
||||||
|
@import './components/Lightbox.scss';
|
||||||
@import './components/MediaQualitySelector.scss';
|
@import './components/MediaQualitySelector.scss';
|
||||||
@import './components/MessageAudio.scss';
|
@import './components/MessageAudio.scss';
|
||||||
@import './components/MessageDetail.scss';
|
@import './components/MessageDetail.scss';
|
||||||
|
|
|
@ -26,13 +26,7 @@ export const AvatarLightbox = ({
|
||||||
onClose,
|
onClose,
|
||||||
}: PropsType): JSX.Element => {
|
}: PropsType): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Lightbox
|
<Lightbox close={onClose} i18n={i18n} media={[]}>
|
||||||
contentType={undefined}
|
|
||||||
close={onClose}
|
|
||||||
i18n={i18n}
|
|
||||||
isViewOnce={false}
|
|
||||||
objectURL=""
|
|
||||||
>
|
|
||||||
<AvatarPreview
|
<AvatarPreview
|
||||||
avatarColor={avatarColor}
|
avatarColor={avatarColor}
|
||||||
avatarPath={avatarPath}
|
avatarPath={avatarPath}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { action } from '@storybook/addon-actions';
|
|
||||||
import { boolean, text } from '@storybook/addon-knobs';
|
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import { number } from '@storybook/addon-knobs';
|
||||||
|
|
||||||
import { Lightbox, Props } from './Lightbox';
|
import enMessages from '../../_locales/en/messages.json';
|
||||||
|
import { Lightbox, PropsType } from './Lightbox';
|
||||||
|
import { MediaItemType } from '../types/MediaItem';
|
||||||
|
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||||
import {
|
import {
|
||||||
AUDIO_MP3,
|
AUDIO_MP3,
|
||||||
IMAGE_JPEG,
|
IMAGE_JPEG,
|
||||||
|
@ -15,123 +18,237 @@ import {
|
||||||
VIDEO_QUICKTIME,
|
VIDEO_QUICKTIME,
|
||||||
stringToMIMEType,
|
stringToMIMEType,
|
||||||
} from '../types/MIME';
|
} from '../types/MIME';
|
||||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
|
||||||
import enMessages from '../../_locales/en/messages.json';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Lightbox', module);
|
const story = storiesOf('Components/Lightbox', module);
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
type OverridePropsMediaItemType = Partial<MediaItemType> & { caption?: string };
|
||||||
caption: text('caption', overrideProps.caption || ''),
|
|
||||||
|
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> = {}): PropsType => ({
|
||||||
close: action('close'),
|
close: action('close'),
|
||||||
contentType: overrideProps.contentType || IMAGE_JPEG,
|
|
||||||
i18n,
|
i18n,
|
||||||
isViewOnce: boolean('isViewOnce', overrideProps.isViewOnce || false),
|
media: overrideProps.media || [],
|
||||||
objectURL: text('objectURL', overrideProps.objectURL || ''),
|
onSave: action('onSave'),
|
||||||
onNext: overrideProps.onNext,
|
selectedIndex: number('selectedIndex', overrideProps.selectedIndex || 0),
|
||||||
onPrevious: overrideProps.onPrevious,
|
|
||||||
onSave: overrideProps.onSave,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Image', () => {
|
story.add('Multimedia', () => {
|
||||||
const props = createProps({
|
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 <Lightbox {...props} />;
|
return <Lightbox {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Image with Caption (normal image)', () => {
|
story.add('Missing Media', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
caption:
|
media: [
|
||||||
'This is the user-provided caption. It can get long and wrap onto multiple lines.',
|
{
|
||||||
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
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 <Lightbox {...props} />;
|
return <Lightbox {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
story.add('Image with Caption (all-white image)', () => {
|
story.add('Single Image', () => (
|
||||||
const props = createProps({
|
<Lightbox
|
||||||
caption:
|
{...createProps({
|
||||||
'This is the user-provided caption. It should be visible on light backgrounds.',
|
media: [
|
||||||
objectURL: '/fixtures/2000x2000-white.png',
|
createMediaItem({
|
||||||
});
|
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
return <Lightbox {...props} />;
|
story.add('Image with Caption (normal image)', () => (
|
||||||
});
|
<Lightbox
|
||||||
|
{...createProps({
|
||||||
|
media: [
|
||||||
|
createMediaItem({
|
||||||
|
caption:
|
||||||
|
'This lighthouse is really cool because there are lots of rocks and there is a tower that has a light and the light is really bright because it shines so much. The day was super duper cloudy and stormy and you can see all the waves hitting against the rocks. Wait? What is that weird red hose line thingy running all the way to the tower? Those rocks look slippery! I bet that water is really cold. I am cold now, can I get a sweater? I wonder where this place is, probably somewhere cold like Coldsgar, Frozenville.',
|
||||||
|
objectURL: '/fixtures/tina-rolf-269345-unsplash.jpg',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
story.add('Video', () => {
|
story.add('Image with Caption (all-white image)', () => (
|
||||||
const props = createProps({
|
<Lightbox
|
||||||
contentType: VIDEO_MP4,
|
{...createProps({
|
||||||
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
media: [
|
||||||
});
|
createMediaItem({
|
||||||
|
caption:
|
||||||
|
'This is the user-provided caption. It should be visible on light backgrounds.',
|
||||||
|
objectURL: '/fixtures/2000x2000-white.png',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
return <Lightbox {...props} />;
|
story.add('Single Video', () => (
|
||||||
});
|
<Lightbox
|
||||||
|
{...createProps({
|
||||||
|
media: [
|
||||||
|
createMediaItem({
|
||||||
|
contentType: VIDEO_MP4,
|
||||||
|
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
story.add('Video with Caption', () => {
|
story.add('Single Video w/caption', () => (
|
||||||
const props = createProps({
|
<Lightbox
|
||||||
caption:
|
{...createProps({
|
||||||
'This is the user-provided caption. It can get long and wrap onto multiple lines.',
|
media: [
|
||||||
contentType: VIDEO_MP4,
|
createMediaItem({
|
||||||
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
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',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
return <Lightbox {...props} />;
|
story.add('Unsupported Image Type', () => (
|
||||||
});
|
<Lightbox
|
||||||
|
{...createProps({
|
||||||
|
media: [
|
||||||
|
createMediaItem({
|
||||||
|
contentType: stringToMIMEType('image/tiff'),
|
||||||
|
objectURL: 'unsupported-image.tiff',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
story.add('Video (View Once)', () => {
|
story.add('Unsupported Video Type', () => (
|
||||||
const props = createProps({
|
<Lightbox
|
||||||
contentType: VIDEO_MP4,
|
{...createProps({
|
||||||
isViewOnce: true,
|
media: [
|
||||||
objectURL: '/fixtures/pixabay-Soap-Bubble-7141.mp4',
|
createMediaItem({
|
||||||
});
|
contentType: VIDEO_QUICKTIME,
|
||||||
|
objectURL: 'unsupported-video.mov',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
return <Lightbox {...props} />;
|
story.add('Unsupported Content', () => (
|
||||||
});
|
<Lightbox
|
||||||
|
{...createProps({
|
||||||
story.add('Unsupported Image Type', () => {
|
media: [
|
||||||
const props = createProps({
|
createMediaItem({
|
||||||
contentType: stringToMIMEType('image/tiff'),
|
contentType: AUDIO_MP3,
|
||||||
objectURL: 'unsupported-image.tiff',
|
objectURL: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
|
||||||
});
|
}),
|
||||||
|
],
|
||||||
return <Lightbox {...props} />;
|
})}
|
||||||
});
|
/>
|
||||||
|
));
|
||||||
story.add('Unsupported Video Type', () => {
|
|
||||||
const props = createProps({
|
|
||||||
contentType: VIDEO_QUICKTIME,
|
|
||||||
objectURL: 'unsupported-video.mov',
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Lightbox {...props} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
story.add('Unsupported ContentType', () => {
|
|
||||||
const props = createProps({
|
|
||||||
contentType: AUDIO_MP3,
|
|
||||||
objectURL: '/fixtures/incompetech-com-Agnus-Dei-X.mp3',
|
|
||||||
});
|
|
||||||
|
|
||||||
return <Lightbox {...props} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
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 <Lightbox {...props} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
story.add('Custom children', () => (
|
story.add('Custom children', () => (
|
||||||
<Lightbox {...createProps({})} contentType={undefined}>
|
<Lightbox {...createProps({})} media={[]}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color: 'white',
|
color: 'white',
|
||||||
|
@ -144,3 +261,30 @@ story.add('Custom children', () => (
|
||||||
</div>
|
</div>
|
||||||
</Lightbox>
|
</Lightbox>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
story.add('Forwarding', () => (
|
||||||
|
<Lightbox {...createProps({})} onForward={action('onForward')} />
|
||||||
|
));
|
||||||
|
|
||||||
|
story.add('Conversation Header', () => (
|
||||||
|
<Lightbox
|
||||||
|
{...createProps({})}
|
||||||
|
getConversation={() => ({
|
||||||
|
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',
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
|
@ -1,291 +1,137 @@
|
||||||
// Copyright 2018-2020 Signal Messenger, LLC
|
// Copyright 2018-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 classNames from 'classnames';
|
||||||
import is from '@sindresorhus/is';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import * as GoogleChrome from '../util/GoogleChrome';
|
import * as GoogleChrome from '../util/GoogleChrome';
|
||||||
import * as MIME from '../types/MIME';
|
import { AttachmentType, isGIF } from '../types/Attachment';
|
||||||
|
import { Avatar, AvatarSize } from './Avatar';
|
||||||
import { formatDuration } from '../util/formatDuration';
|
import { ConversationType } from '../state/ducks/conversations';
|
||||||
|
import { IMAGE_PNG, isImage, isVideo } from '../types/MIME';
|
||||||
import { LocalizerType } from '../types/Util';
|
import { LocalizerType } from '../types/Util';
|
||||||
|
import { MediaItemType, MessageAttributesType } from '../types/MediaItem';
|
||||||
|
|
||||||
const Colors = {
|
export type PropsType = {
|
||||||
ICON_SECONDARY: '#b9b9b9',
|
|
||||||
};
|
|
||||||
|
|
||||||
const colorSVG = (url: string, color: string) => {
|
|
||||||
return {
|
|
||||||
WebkitMask: `url(${url}) no-repeat center`,
|
|
||||||
WebkitMaskSize: '100%',
|
|
||||||
backgroundColor: color,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
contentType: MIME.MIMEType | undefined;
|
getConversation?: (id: string) => ConversationType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
objectURL: string;
|
media: Array<MediaItemType>;
|
||||||
caption?: string;
|
onForward?: (messageId: string) => void;
|
||||||
isViewOnce: boolean;
|
onSave?: (options: {
|
||||||
loop?: boolean;
|
attachment: AttachmentType;
|
||||||
onNext?: () => void;
|
message: MessageAttributesType;
|
||||||
onPrevious?: () => void;
|
index: number;
|
||||||
onSave?: () => void;
|
}) => void;
|
||||||
};
|
selectedIndex?: number;
|
||||||
type State = {
|
|
||||||
videoTime?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CONTROLS_WIDTH = 50;
|
export function Lightbox({
|
||||||
const CONTROLS_SPACING = 10;
|
children,
|
||||||
|
close,
|
||||||
|
getConversation,
|
||||||
|
media,
|
||||||
|
i18n,
|
||||||
|
onForward,
|
||||||
|
onSave,
|
||||||
|
selectedIndex: initialSelectedIndex,
|
||||||
|
}: PropsType): JSX.Element | null {
|
||||||
|
const [root, setRoot] = React.useState<HTMLElement | undefined>();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number>(
|
||||||
|
initialSelectedIndex || 0
|
||||||
|
);
|
||||||
|
|
||||||
const styles = {
|
const [previousFocus, setPreviousFocus] = useState<HTMLElement | undefined>();
|
||||||
container: {
|
const [zoomed, setZoomed] = useState(false);
|
||||||
display: 'flex',
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
flexDirection: 'column',
|
const focusRef = useRef<HTMLDivElement | null>(null);
|
||||||
position: 'absolute',
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
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',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type IconButtonProps = {
|
const restorePreviousFocus = useCallback(() => {
|
||||||
i18n: LocalizerType;
|
if (previousFocus && previousFocus.focus) {
|
||||||
onClick?: () => void;
|
previousFocus.focus();
|
||||||
style?: React.CSSProperties;
|
|
||||||
type: 'save' | 'close' | 'previous' | 'next';
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconButton = ({ i18n, onClick, style, type }: IconButtonProps) => {
|
|
||||||
const clickHandler = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!onClick) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
}, [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 (
|
const handleForward = () => {
|
||||||
<button
|
close();
|
||||||
onClick={clickHandler}
|
const mediaItem = media[selectedIndex];
|
||||||
className={classNames('iconButton', type)}
|
onForward?.(mediaItem.message.id);
|
||||||
style={style}
|
};
|
||||||
aria-label={i18n(type)}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const IconButtonPlaceholder = () => (
|
const onKeyDown = useCallback(
|
||||||
<div style={styles.iconButtonPlaceholder} />
|
(event: KeyboardEvent) => {
|
||||||
);
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
if (zoomed) {
|
||||||
|
setZoomed(false);
|
||||||
|
} else {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
const Icon = ({
|
event.preventDefault();
|
||||||
i18n,
|
event.stopPropagation();
|
||||||
onClick,
|
|
||||||
url,
|
|
||||||
}: {
|
|
||||||
i18n: LocalizerType;
|
|
||||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
|
||||||
url: string;
|
|
||||||
}) => (
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
...styles.object,
|
|
||||||
...colorSVG(url, Colors.ICON_SECONDARY),
|
|
||||||
maxWidth: 200,
|
|
||||||
}}
|
|
||||||
onClick={onClick}
|
|
||||||
aria-label={i18n('unsupportedAttachment')}
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
export class Lightbox extends React.Component<Props, State> {
|
break;
|
||||||
public readonly containerRef = React.createRef<HTMLDivElement>();
|
|
||||||
|
|
||||||
public readonly videoRef = React.createRef<HTMLVideoElement>();
|
case 'ArrowLeft':
|
||||||
|
if (onPrevious) {
|
||||||
|
onPrevious();
|
||||||
|
|
||||||
public readonly focusRef = React.createRef<HTMLDivElement>();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
public previousFocus: HTMLElement | null = null;
|
case 'ArrowRight':
|
||||||
|
if (onNext) {
|
||||||
|
onNext();
|
||||||
|
|
||||||
public constructor(props: Props) {
|
event.preventDefault();
|
||||||
super(props);
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
this.state = {};
|
default:
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.previousFocus = document.activeElement as HTMLElement;
|
|
||||||
|
|
||||||
const { isViewOnce } = this.props;
|
|
||||||
|
|
||||||
const useCapture = true;
|
|
||||||
document.addEventListener('keydown', this.onKeyDown, useCapture);
|
|
||||||
|
|
||||||
const video = this.getVideo();
|
|
||||||
if (video && isViewOnce) {
|
|
||||||
video.addEventListener('timeupdate', this.onTimeUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait until we're added to the DOM. ConversationView first creates this view, then
|
|
||||||
// appends its elements into the DOM.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.playVideo();
|
|
||||||
|
|
||||||
if (this.focusRef && this.focusRef.current) {
|
|
||||||
this.focusRef.current.focus();
|
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
[close, onNext, onPrevious, zoomed]
|
||||||
|
);
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
const stopPropagationAndClose = (event: MouseEvent<HTMLElement>) => {
|
||||||
if (this.previousFocus && this.previousFocus.focus) {
|
event.stopPropagation();
|
||||||
this.previousFocus.focus();
|
close();
|
||||||
}
|
};
|
||||||
|
|
||||||
const { isViewOnce } = this.props;
|
const playVideo = () => {
|
||||||
|
const video = videoRef.current;
|
||||||
const useCapture = true;
|
|
||||||
document.removeEventListener('keydown', this.onKeyDown, useCapture);
|
|
||||||
|
|
||||||
const video = this.getVideo();
|
|
||||||
if (video && isViewOnce) {
|
|
||||||
video.removeEventListener('timeupdate', this.onTimeUpdate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public getVideo(): HTMLVideoElement | null {
|
|
||||||
if (!this.videoRef) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { current } = this.videoRef;
|
|
||||||
if (!current) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public playVideo(): void {
|
|
||||||
const video = this.getVideo();
|
|
||||||
if (!video) {
|
if (!video) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -295,238 +141,333 @@ export class Lightbox extends React.Component<Props, State> {
|
||||||
} else {
|
} else {
|
||||||
video.pause();
|
video.pause();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
public render(): JSX.Element {
|
useEffect(() => {
|
||||||
const {
|
const div = document.createElement('div');
|
||||||
caption,
|
document.body.appendChild(div);
|
||||||
children,
|
setRoot(div);
|
||||||
contentType,
|
|
||||||
i18n,
|
|
||||||
isViewOnce,
|
|
||||||
loop = false,
|
|
||||||
objectURL,
|
|
||||||
onNext,
|
|
||||||
onPrevious,
|
|
||||||
onSave,
|
|
||||||
} = this.props;
|
|
||||||
const { videoTime } = this.state;
|
|
||||||
|
|
||||||
return (
|
return () => {
|
||||||
<div
|
document.body.removeChild(div);
|
||||||
className="module-lightbox"
|
setRoot(undefined);
|
||||||
style={styles.container}
|
};
|
||||||
onClick={this.onContainerClick}
|
}, []);
|
||||||
onKeyUp={this.onContainerKeyUp}
|
|
||||||
ref={this.containerRef}
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
<div style={styles.mainContainer} tabIndex={-1} ref={this.focusRef}>
|
|
||||||
<div style={styles.controlsOffsetPlaceholder} />
|
|
||||||
<div style={styles.objectContainer}>
|
|
||||||
{!is.undefined(contentType)
|
|
||||||
? this.renderObject({
|
|
||||||
objectURL,
|
|
||||||
contentType,
|
|
||||||
i18n,
|
|
||||||
isViewOnce,
|
|
||||||
loop,
|
|
||||||
})
|
|
||||||
: children}
|
|
||||||
{caption ? <div style={styles.caption}>{caption}</div> : null}
|
|
||||||
</div>
|
|
||||||
<div style={styles.controls}>
|
|
||||||
<IconButton i18n={i18n} type="close" onClick={this.onClose} />
|
|
||||||
{onSave ? (
|
|
||||||
<IconButton
|
|
||||||
i18n={i18n}
|
|
||||||
type="save"
|
|
||||||
onClick={onSave}
|
|
||||||
style={styles.saveButton}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{isViewOnce && videoTime && is.number(videoTime) ? (
|
|
||||||
<div style={styles.navigationContainer}>
|
|
||||||
<div style={styles.timestampPill}>{formatDuration(videoTime)}</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={styles.navigationContainer}>
|
|
||||||
{onPrevious ? (
|
|
||||||
<IconButton i18n={i18n} type="previous" onClick={onPrevious} />
|
|
||||||
) : (
|
|
||||||
<IconButtonPlaceholder />
|
|
||||||
)}
|
|
||||||
{onNext ? (
|
|
||||||
<IconButton i18n={i18n} type="next" onClick={onNext} />
|
|
||||||
) : (
|
|
||||||
<IconButtonPlaceholder />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly renderObject = ({
|
useEffect(() => {
|
||||||
objectURL,
|
if (!previousFocus) {
|
||||||
contentType,
|
setPreviousFocus(document.activeElement as HTMLElement);
|
||||||
i18n,
|
|
||||||
isViewOnce,
|
|
||||||
loop,
|
|
||||||
}: {
|
|
||||||
objectURL: string;
|
|
||||||
contentType: MIME.MIMEType;
|
|
||||||
i18n: LocalizerType;
|
|
||||||
isViewOnce: boolean;
|
|
||||||
loop: boolean;
|
|
||||||
}) => {
|
|
||||||
const isImageTypeSupported = GoogleChrome.isImageTypeSupported(contentType);
|
|
||||||
if (isImageTypeSupported) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={styles.buttonContainer}
|
|
||||||
onClick={this.onObjectClick}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
alt={i18n('lightboxImageAlt')}
|
|
||||||
style={styles.img}
|
|
||||||
src={objectURL}
|
|
||||||
onContextMenu={this.onContextMenu}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}, [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);
|
const isVideoTypeSupported = GoogleChrome.isVideoTypeSupported(contentType);
|
||||||
if (isVideoTypeSupported) {
|
const isUnsupportedImageType =
|
||||||
return (
|
!isImageTypeSupported && isImage(contentType);
|
||||||
|
const isUnsupportedVideoType =
|
||||||
|
!isVideoTypeSupported && isVideo(contentType);
|
||||||
|
|
||||||
|
if (isImageTypeSupported) {
|
||||||
|
if (objectURL) {
|
||||||
|
content = (
|
||||||
|
<button
|
||||||
|
className="Lightbox__zoom-button"
|
||||||
|
onClick={() => setZoomed(!zoomed)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt={i18n('lightboxImageAlt')}
|
||||||
|
className="Lightbox__object"
|
||||||
|
onContextMenu={(event: MouseEvent<HTMLImageElement>) => {
|
||||||
|
// These are the only image types supported by Electron's NativeImage
|
||||||
|
if (
|
||||||
|
event &&
|
||||||
|
contentType !== IMAGE_PNG &&
|
||||||
|
!/image\/jpe?g/g.test(contentType)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
src={objectURL}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = (
|
||||||
|
<button
|
||||||
|
aria-label={i18n('lightboxImageAlt')}
|
||||||
|
className={classNames({
|
||||||
|
Lightbox__object: true,
|
||||||
|
Lightbox__unsupported: true,
|
||||||
|
'Lightbox__unsupported--missing': true,
|
||||||
|
})}
|
||||||
|
onClick={stopPropagationAndClose}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (isVideoTypeSupported) {
|
||||||
|
const shouldLoop = loop || isGIF([attachment]);
|
||||||
|
content = (
|
||||||
<video
|
<video
|
||||||
ref={this.videoRef}
|
className="Lightbox__object"
|
||||||
loop={loop || isViewOnce}
|
controls={!shouldLoop}
|
||||||
controls={!loop && !isViewOnce}
|
|
||||||
style={styles.object}
|
|
||||||
key={objectURL}
|
key={objectURL}
|
||||||
|
loop={shouldLoop}
|
||||||
|
ref={videoRef}
|
||||||
>
|
>
|
||||||
<source src={objectURL} />
|
<source src={objectURL} />
|
||||||
</video>
|
</video>
|
||||||
);
|
);
|
||||||
|
} else if (isUnsupportedImageType || isUnsupportedVideoType) {
|
||||||
|
content = (
|
||||||
|
<button
|
||||||
|
aria-label={i18n('unsupportedAttachment')}
|
||||||
|
className={classNames({
|
||||||
|
Lightbox__object: true,
|
||||||
|
Lightbox__unsupported: true,
|
||||||
|
'Lightbox__unsupported--image': isUnsupportedImageType,
|
||||||
|
'Lightbox__unsupported--video': isUnsupportedVideoType,
|
||||||
|
})}
|
||||||
|
onClick={stopPropagationAndClose}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.log.info('Lightbox: Unexpected content type', { contentType });
|
||||||
|
|
||||||
|
content = (
|
||||||
|
<button
|
||||||
|
aria-label={i18n('unsupportedAttachment')}
|
||||||
|
className="Lightbox__object Lightbox__unsupported Lightbox__unsupported--file"
|
||||||
|
onClick={stopPropagationAndClose}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isUnsupportedImageType =
|
const hasNext = selectedIndex < media.length - 1;
|
||||||
!isImageTypeSupported && MIME.isImage(contentType);
|
const hasPrevious = selectedIndex > 0;
|
||||||
const isUnsupportedVideoType =
|
|
||||||
!isVideoTypeSupported && MIME.isVideo(contentType);
|
|
||||||
if (isUnsupportedImageType || isUnsupportedVideoType) {
|
|
||||||
const iconUrl = isUnsupportedVideoType
|
|
||||||
? 'images/movie.svg'
|
|
||||||
: 'images/image.svg';
|
|
||||||
|
|
||||||
return <Icon i18n={i18n} url={iconUrl} onClick={this.onObjectClick} />;
|
return root
|
||||||
}
|
? createPortal(
|
||||||
|
<div
|
||||||
|
className="Lightbox Lightbox__container"
|
||||||
|
onClick={(event: MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (containerRef && event.target !== containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
}}
|
||||||
|
onKeyUp={(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (
|
||||||
|
(containerRef && event.target !== containerRef.current) ||
|
||||||
|
event.keyCode !== 27
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
window.log.info('Lightbox: Unexpected content type', { contentType });
|
close();
|
||||||
|
}}
|
||||||
return (
|
ref={containerRef}
|
||||||
<Icon i18n={i18n} onClick={this.onObjectClick} url="images/file.svg" />
|
role="presentation"
|
||||||
);
|
>
|
||||||
};
|
<div
|
||||||
|
className="Lightbox__main-container"
|
||||||
private readonly onContextMenu = (
|
tabIndex={-1}
|
||||||
event: React.MouseEvent<HTMLImageElement>
|
ref={focusRef}
|
||||||
) => {
|
>
|
||||||
const { contentType = '' } = this.props;
|
{!zoomed && (
|
||||||
|
<div className="Lightbox__header">
|
||||||
// These are the only image types supported by Electron's NativeImage
|
{getConversation ? (
|
||||||
if (
|
<LightboxHeader
|
||||||
event &&
|
getConversation={getConversation}
|
||||||
contentType !== 'image/png' &&
|
i18n={i18n}
|
||||||
!/image\/jpe?g/g.test(contentType)
|
message={message}
|
||||||
) {
|
/>
|
||||||
event.preventDefault();
|
) : (
|
||||||
}
|
<div />
|
||||||
};
|
)}
|
||||||
|
<div className="Lightbox__controls">
|
||||||
private readonly onClose = () => {
|
{onForward ? (
|
||||||
const { close } = this.props;
|
<button
|
||||||
if (!close) {
|
aria-label={i18n('forwardMessage')}
|
||||||
return;
|
className="Lightbox__button Lightbox__button--forward"
|
||||||
}
|
onClick={handleForward}
|
||||||
|
type="button"
|
||||||
close();
|
/>
|
||||||
};
|
) : null}
|
||||||
|
{onSave ? (
|
||||||
private readonly onTimeUpdate = () => {
|
<button
|
||||||
const video = this.getVideo();
|
aria-label={i18n('save')}
|
||||||
if (!video) {
|
className="Lightbox__button Lightbox__button--save"
|
||||||
return;
|
onClick={handleSave}
|
||||||
}
|
type="button"
|
||||||
this.setState({
|
/>
|
||||||
videoTime: video.currentTime,
|
) : null}
|
||||||
});
|
<button
|
||||||
};
|
aria-label={i18n('close')}
|
||||||
|
className="Lightbox__button Lightbox__button--close"
|
||||||
private readonly onKeyDown = (event: KeyboardEvent) => {
|
onClick={close}
|
||||||
const { onNext, onPrevious } = this.props;
|
type="button"
|
||||||
switch (event.key) {
|
/>
|
||||||
case 'Escape':
|
</div>
|
||||||
this.onClose();
|
</div>
|
||||||
|
)}
|
||||||
event.preventDefault();
|
<div
|
||||||
event.stopPropagation();
|
className={classNames('Lightbox__object--container', {
|
||||||
|
'Lightbox__object--container--zoomed': zoomed,
|
||||||
break;
|
})}
|
||||||
|
>
|
||||||
case 'ArrowLeft':
|
{content}
|
||||||
if (onPrevious) {
|
</div>
|
||||||
onPrevious();
|
{hasPrevious && (
|
||||||
|
<div className="Lightbox__nav-prev">
|
||||||
event.preventDefault();
|
<button
|
||||||
event.stopPropagation();
|
aria-label={i18n('previous')}
|
||||||
}
|
className="Lightbox__button Lightbox__button--previous"
|
||||||
break;
|
disabled={zoomed}
|
||||||
|
onClick={onPrevious}
|
||||||
case 'ArrowRight':
|
type="button"
|
||||||
if (onNext) {
|
/>
|
||||||
onNext();
|
</div>
|
||||||
|
)}
|
||||||
event.preventDefault();
|
{hasNext && (
|
||||||
event.stopPropagation();
|
<div className="Lightbox__nav-next">
|
||||||
}
|
<button
|
||||||
break;
|
aria-label={i18n('next')}
|
||||||
|
className="Lightbox__button Lightbox__button--next"
|
||||||
default:
|
disabled={zoomed}
|
||||||
}
|
onClick={onNext}
|
||||||
};
|
type="button"
|
||||||
|
/>
|
||||||
private readonly onContainerClick = (
|
</div>
|
||||||
event: React.MouseEvent<HTMLDivElement>
|
)}
|
||||||
) => {
|
</div>
|
||||||
if (this.containerRef && event.target !== this.containerRef.current) {
|
{!zoomed && (
|
||||||
return;
|
<div className="Lightbox__footer">
|
||||||
}
|
{caption ? (
|
||||||
this.onClose();
|
<div className="Lightbox__caption">{caption}</div>
|
||||||
};
|
) : null}
|
||||||
|
{media.length > 1 && (
|
||||||
private readonly onContainerKeyUp = (
|
<div className="Lightbox__thumbnails--container">
|
||||||
event: React.KeyboardEvent<HTMLDivElement>
|
<div
|
||||||
) => {
|
className="Lightbox__thumbnails"
|
||||||
if (
|
style={{
|
||||||
(this.containerRef && event.target !== this.containerRef.current) ||
|
marginLeft:
|
||||||
event.keyCode !== 27
|
0 - (selectedIndex * 64 + selectedIndex * 8 + 32),
|
||||||
) {
|
}}
|
||||||
return;
|
>
|
||||||
}
|
{media.map((item, index) => (
|
||||||
|
<button
|
||||||
this.onClose();
|
className={classNames({
|
||||||
};
|
Lightbox__thumbnail: true,
|
||||||
|
'Lightbox__thumbnail--selected':
|
||||||
private readonly onObjectClick = (
|
index === selectedIndex,
|
||||||
event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
|
})}
|
||||||
) => {
|
key={item.thumbnailObjectUrl}
|
||||||
event.stopPropagation();
|
type="button"
|
||||||
this.onClose();
|
onClick={() => setSelectedIndex(index)}
|
||||||
};
|
>
|
||||||
|
{item.thumbnailObjectUrl ? (
|
||||||
|
<img
|
||||||
|
alt={i18n('lightboxImageAlt')}
|
||||||
|
src={item.thumbnailObjectUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="Lightbox__thumbnail--unavailable" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
root
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LightboxHeader({
|
||||||
|
getConversation,
|
||||||
|
i18n,
|
||||||
|
message,
|
||||||
|
}: {
|
||||||
|
getConversation: (id: string) => ConversationType;
|
||||||
|
i18n: LocalizerType;
|
||||||
|
message: MessageAttributesType;
|
||||||
|
}): JSX.Element {
|
||||||
|
const conversation = getConversation(message.conversationId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="Lightbox__header--container">
|
||||||
|
<div className="Lightbox__header--avatar">
|
||||||
|
<Avatar
|
||||||
|
acceptedMessageRequest={conversation.acceptedMessageRequest}
|
||||||
|
avatarPath={conversation.avatarPath}
|
||||||
|
color={conversation.color}
|
||||||
|
conversationType={conversation.type}
|
||||||
|
i18n={i18n}
|
||||||
|
isMe={conversation.isMe}
|
||||||
|
name={conversation.name}
|
||||||
|
phoneNumber={conversation.e164}
|
||||||
|
profileName={conversation.profileName}
|
||||||
|
sharedGroupNames={conversation.sharedGroupNames}
|
||||||
|
size={AvatarSize.THIRTY_TWO}
|
||||||
|
title={conversation.title}
|
||||||
|
unblurredAvatarPath={conversation.unblurredAvatarPath}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="Lightbox__header--content">
|
||||||
|
<div className="Lightbox__header--name">{conversation.title}</div>
|
||||||
|
<div className="Lightbox__header--timestamp">
|
||||||
|
{moment(message.received_at_ms).format('L LT')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> = {}): 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 <LightboxGallery {...props} />;
|
|
||||||
});
|
|
||||||
|
|
||||||
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 <LightboxGallery {...props} />;
|
|
||||||
});
|
|
|
@ -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<MediaItemType>;
|
|
||||||
onSave?: (options: {
|
|
||||||
attachment: AttachmentType;
|
|
||||||
message: Message;
|
|
||||||
index: number;
|
|
||||||
}) => void;
|
|
||||||
selectedIndex: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
selectedIndex: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class LightboxGallery extends React.Component<Props, State> {
|
|
||||||
public static defaultProps: Partial<Props> = {
|
|
||||||
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 (
|
|
||||||
<Lightbox
|
|
||||||
caption={captionCallback}
|
|
||||||
close={close}
|
|
||||||
contentType={selectedMedia.contentType}
|
|
||||||
i18n={i18n}
|
|
||||||
isViewOnce={false}
|
|
||||||
objectURL={objectURL}
|
|
||||||
onNext={onNext}
|
|
||||||
onPrevious={onPrevious}
|
|
||||||
onSave={saveCallback}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 });
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import { assert } from '../../../util/assert';
|
||||||
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
import { getMutedUntilText } from '../../../util/getMutedUntilText';
|
||||||
|
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
|
||||||
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
|
import { DisappearingTimerSelect } from '../../DisappearingTimerSelect';
|
||||||
|
|
|
@ -17,7 +17,7 @@ import {
|
||||||
createPreparedMediaItems,
|
createPreparedMediaItems,
|
||||||
createRandomMedia,
|
createRandomMedia,
|
||||||
} from '../media-gallery/AttachmentSection.stories';
|
} from '../media-gallery/AttachmentSection.stories';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import React from 'react';
|
||||||
|
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
|
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
import { ConversationType } from '../../../state/ducks/conversations';
|
import { ConversationType } from '../../../state/ducks/conversations';
|
||||||
|
|
||||||
import { PanelSection } from './PanelSection';
|
import { PanelSection } from './PanelSection';
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { random, range, sample, sortBy } from 'lodash';
|
||||||
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||||
import enMessages from '../../../../_locales/en/messages.json';
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
import { MIMEType } from '../../../types/MIME';
|
import { MIMEType } from '../../../types/MIME';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
|
|
||||||
import { AttachmentSection, Props } from './AttachmentSection';
|
import { AttachmentSection, Props } from './AttachmentSection';
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ const createRandomFile = (
|
||||||
return {
|
return {
|
||||||
contentType,
|
contentType,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '123',
|
||||||
id: random(now).toString(),
|
id: random(now).toString(),
|
||||||
received_at: Math.floor(Math.random() * 10),
|
received_at: Math.floor(Math.random() * 10),
|
||||||
received_at_ms: random(startTime, startTime + timeWindow),
|
received_at_ms: random(startTime, startTime + timeWindow),
|
||||||
|
|
|
@ -6,7 +6,7 @@ import React from 'react';
|
||||||
import { DocumentListItem } from './DocumentListItem';
|
import { DocumentListItem } from './DocumentListItem';
|
||||||
import { ItemClickEvent } from './types/ItemClickEvent';
|
import { ItemClickEvent } from './types/ItemClickEvent';
|
||||||
import { MediaGridItem } from './MediaGridItem';
|
import { MediaGridItem } from './MediaGridItem';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
import { missingCaseError } from '../../../util/missingCaseError';
|
import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
documents: Array<MediaItemType>;
|
documents: Array<MediaItemType>;
|
||||||
|
|
|
@ -8,12 +8,11 @@ import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../../js/modules/i18n';
|
||||||
import enMessages from '../../../../_locales/en/messages.json';
|
import enMessages from '../../../../_locales/en/messages.json';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
import { AttachmentType } from '../../../types/Attachment';
|
import { AttachmentType } from '../../../types/Attachment';
|
||||||
import { stringToMIMEType } from '../../../types/MIME';
|
import { stringToMIMEType } from '../../../types/MIME';
|
||||||
|
|
||||||
import { MediaGridItem, Props } from './MediaGridItem';
|
import { MediaGridItem, Props } from './MediaGridItem';
|
||||||
import { Message } from './types/Message';
|
|
||||||
|
|
||||||
const i18n = setupI18n('en', enMessages);
|
const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
|
@ -45,7 +44,13 @@ const createMediaItem = (
|
||||||
),
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
attachment: {} as AttachmentType, // attachment not useful in the component
|
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', () => {
|
story.add('Image', () => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {
|
||||||
isVideoTypeSupported,
|
isVideoTypeSupported,
|
||||||
} from '../../../util/GoogleChrome';
|
} from '../../../util/GoogleChrome';
|
||||||
import { LocalizerType } from '../../../types/Util';
|
import { LocalizerType } from '../../../types/Util';
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
mediaItem: MediaItemType;
|
mediaItem: MediaItemType;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { compact, groupBy, sortBy } from 'lodash';
|
import { compact, groupBy, sortBy } from 'lodash';
|
||||||
|
|
||||||
import { MediaItemType } from '../../LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
import { getMessageTimestamp } from '../../../util/getMessageTimestamp';
|
||||||
|
|
||||||
// import { missingCaseError } from '../../../util/missingCaseError';
|
// import { missingCaseError } from '../../../util/missingCaseError';
|
||||||
|
|
|
@ -42,7 +42,7 @@ import {
|
||||||
} from '../../model-types.d';
|
} from '../../model-types.d';
|
||||||
import { BodyRangeType } from '../../types/Util';
|
import { BodyRangeType } from '../../types/Util';
|
||||||
import { CallMode } from '../../types/Calling';
|
import { CallMode } from '../../types/Calling';
|
||||||
import { MediaItemType } from '../../components/LightboxGallery';
|
import { MediaItemType } from '../../types/MediaItem';
|
||||||
import {
|
import {
|
||||||
getGroupSizeRecommendedLimit,
|
getGroupSizeRecommendedLimit,
|
||||||
getGroupSizeHardLimit,
|
getGroupSizeHardLimit,
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from '../selectors/conversations';
|
} from '../selectors/conversations';
|
||||||
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
import { getGroupMemberships } from '../../util/getGroupMemberships';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { MediaItemType } from '../../components/LightboxGallery';
|
import { MediaItemType } from '../../types/MediaItem';
|
||||||
import { assert } from '../../util/assert';
|
import { assert } from '../../util/assert';
|
||||||
import { SignalService as Proto } from '../../protobuf';
|
import { SignalService as Proto } from '../../protobuf';
|
||||||
|
|
||||||
|
|
|
@ -9,12 +9,13 @@ import {
|
||||||
groupMediaItemsByDate,
|
groupMediaItemsByDate,
|
||||||
Section,
|
Section,
|
||||||
} from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
|
} from '../../../components/conversation/media-gallery/groupMediaItemsByDate';
|
||||||
import { MediaItemType } from '../../../components/LightboxGallery';
|
import { MediaItemType } from '../../../types/MediaItem';
|
||||||
|
|
||||||
const toMediaItem = (date: Date): MediaItemType => ({
|
const toMediaItem = (date: Date): MediaItemType => ({
|
||||||
objectURL: date.toUTCString(),
|
objectURL: date.toUTCString(),
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: date.getTime(),
|
received_at: date.getTime(),
|
||||||
received_at_ms: date.getTime(),
|
received_at_ms: date.getTime(),
|
||||||
|
@ -56,6 +57,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Thu, 12 Apr 2018 12:00:00 GMT',
|
objectURL: 'Thu, 12 Apr 2018 12:00:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523534400000,
|
received_at: 1523534400000,
|
||||||
received_at_ms: 1523534400000,
|
received_at_ms: 1523534400000,
|
||||||
|
@ -71,6 +73,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT',
|
objectURL: 'Thu, 12 Apr 2018 00:01:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523491260000,
|
received_at: 1523491260000,
|
||||||
received_at_ms: 1523491260000,
|
received_at_ms: 1523491260000,
|
||||||
|
@ -91,6 +94,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Wed, 11 Apr 2018 23:59:00 GMT',
|
objectURL: 'Wed, 11 Apr 2018 23:59:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523491140000,
|
received_at: 1523491140000,
|
||||||
received_at_ms: 1523491140000,
|
received_at_ms: 1523491140000,
|
||||||
|
@ -111,6 +115,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Mon, 09 Apr 2018 00:01:00 GMT',
|
objectURL: 'Mon, 09 Apr 2018 00:01:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523232060000,
|
received_at: 1523232060000,
|
||||||
received_at_ms: 1523232060000,
|
received_at_ms: 1523232060000,
|
||||||
|
@ -131,6 +136,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Sun, 08 Apr 2018 23:59:00 GMT',
|
objectURL: 'Sun, 08 Apr 2018 23:59:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1523231940000,
|
received_at: 1523231940000,
|
||||||
received_at_ms: 1523231940000,
|
received_at_ms: 1523231940000,
|
||||||
|
@ -146,6 +152,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT',
|
objectURL: 'Sun, 01 Apr 2018 00:01:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1522540860000,
|
received_at: 1522540860000,
|
||||||
received_at_ms: 1522540860000,
|
received_at_ms: 1522540860000,
|
||||||
|
@ -168,6 +175,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Sat, 31 Mar 2018 23:59:00 GMT',
|
objectURL: 'Sat, 31 Mar 2018 23:59:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1522540740000,
|
received_at: 1522540740000,
|
||||||
received_at_ms: 1522540740000,
|
received_at_ms: 1522540740000,
|
||||||
|
@ -183,6 +191,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT',
|
objectURL: 'Thu, 01 Mar 2018 14:00:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1519912800000,
|
received_at: 1519912800000,
|
||||||
received_at_ms: 1519912800000,
|
received_at_ms: 1519912800000,
|
||||||
|
@ -205,6 +214,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Mon, 28 Feb 2011 23:59:00 GMT',
|
objectURL: 'Mon, 28 Feb 2011 23:59:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1298937540000,
|
received_at: 1298937540000,
|
||||||
received_at_ms: 1298937540000,
|
received_at_ms: 1298937540000,
|
||||||
|
@ -220,6 +230,7 @@ describe('groupMediaItemsByDate', () => {
|
||||||
objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT',
|
objectURL: 'Tue, 01 Feb 2011 10:00:00 GMT',
|
||||||
index: 0,
|
index: 0,
|
||||||
message: {
|
message: {
|
||||||
|
conversationId: '1234',
|
||||||
id: 'id',
|
id: 'id',
|
||||||
received_at: 1296554400000,
|
received_at: 1296554400000,
|
||||||
received_at_ms: 1296554400000,
|
received_at_ms: 1296554400000,
|
||||||
|
|
|
@ -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<AttachmentType>;
|
||||||
|
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;
|
||||||
|
};
|
|
@ -13538,27 +13538,46 @@
|
||||||
"updated": "2020-07-21T18:34:59.251Z"
|
"updated": "2020-07-21T18:34:59.251Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/Lightbox.js",
|
"path": "ts/components/Lightbox.js",
|
||||||
"line": " this.containerRef = react_1.default.createRef();",
|
"line": " const containerRef = react_1.useRef(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-06T19:56:38.557Z",
|
"updated": "2021-08-23T18:39:37.081Z"
|
||||||
"reasonDetail": "Used to double-check outside clicks"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/Lightbox.js",
|
"path": "ts/components/Lightbox.js",
|
||||||
"line": " this.focusRef = react_1.default.createRef();",
|
"line": " const focusRef = react_1.useRef(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-06T19:56:38.557Z",
|
"updated": "2021-08-23T18:39:37.081Z"
|
||||||
"reasonDetail": "Used to manage focus"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-createRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/Lightbox.js",
|
"path": "ts/components/Lightbox.js",
|
||||||
"line": " this.videoRef = react_1.default.createRef();",
|
"line": " const videoRef = react_1.useRef(null);",
|
||||||
"reasonCategory": "usageTrusted",
|
"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<HTMLDivElement | null>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"updated": "2021-08-23T18:39:37.081Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rule": "React-useRef",
|
||||||
|
"path": "ts/components/Lightbox.tsx",
|
||||||
|
"line": " const focusRef = useRef<HTMLDivElement | null>(null);",
|
||||||
|
"reasonCategory": "usageTrusted",
|
||||||
|
"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",
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
} from '../model-types.d';
|
} from '../model-types.d';
|
||||||
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
import { LinkPreviewType } from '../types/message/LinkPreviews';
|
||||||
import { MediaItemType } from '../components/LightboxGallery';
|
import { MediaItemType } from '../types/MediaItem';
|
||||||
import { MessageModel } from '../models/messages';
|
import { MessageModel } from '../models/messages';
|
||||||
import { assert } from '../util/assert';
|
import { assert } from '../util/assert';
|
||||||
import { maybeParseUrl } from '../util/url';
|
import { maybeParseUrl } from '../util/url';
|
||||||
|
@ -47,7 +47,10 @@ import {
|
||||||
isTapToView,
|
isTapToView,
|
||||||
} from '../state/selectors/message';
|
} from '../state/selectors/message';
|
||||||
import { isMessageUnread } from '../util/isMessageUnread';
|
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 { ConversationDetailsMembershipList } from '../components/conversation/conversation-details/ConversationDetailsMembershipList';
|
||||||
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
|
import { showSafetyNumberChangeDialog } from '../shims/showSafetyNumberChangeDialog';
|
||||||
import {
|
import {
|
||||||
|
@ -2654,7 +2657,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
);
|
);
|
||||||
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
||||||
className: 'lightbox-wrapper',
|
className: 'lightbox-wrapper',
|
||||||
Component: window.Signal.Components.LightboxGallery,
|
Component: window.Signal.Components.Lightbox,
|
||||||
props: {
|
props: {
|
||||||
media,
|
media,
|
||||||
onSave: saveAttachment,
|
onSave: saveAttachment,
|
||||||
|
@ -3039,10 +3042,10 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: DESKTOP-1133 (DRY up these lightboxes)
|
|
||||||
showLightboxForMedia(
|
showLightboxForMedia(
|
||||||
selectedMediaItem: WhatIsThis,
|
selectedMediaItem: MediaItemType,
|
||||||
media: Array<WhatIsThis> = []
|
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({
|
||||||
|
@ -3065,11 +3068,14 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
|
|
||||||
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
this.lightboxGalleryView = new Whisper.ReactWrapperView({
|
||||||
className: 'lightbox-wrapper',
|
className: 'lightbox-wrapper',
|
||||||
Component: window.Signal.Components.LightboxGallery,
|
Component: window.Signal.Components.Lightbox,
|
||||||
props: {
|
props: {
|
||||||
|
getConversation: getConversationSelector(window.reduxStore.getState()),
|
||||||
|
loop,
|
||||||
media,
|
media,
|
||||||
|
onForward: this.showForwardMessageModal.bind(this),
|
||||||
onSave,
|
onSave,
|
||||||
selectedIndex,
|
selectedIndex: selectedIndex >= 0 ? selectedIndex : 0,
|
||||||
},
|
},
|
||||||
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
|
onClose: () => window.Signal.Backbone.Views.Lightbox.hide(),
|
||||||
});
|
});
|
||||||
|
@ -3096,7 +3102,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { contentType, path } = attachment;
|
const { contentType } = attachment;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!window.Signal.Util.GoogleChrome.isImageTypeSupported(contentType) &&
|
!window.Signal.Util.GoogleChrome.isImageTypeSupported(contentType) &&
|
||||||
|
@ -3118,71 +3124,23 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
contentType: item.contentType,
|
contentType: item.contentType,
|
||||||
loop,
|
loop,
|
||||||
index,
|
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,
|
attachment: item,
|
||||||
|
thumbnailObjectUrl:
|
||||||
|
item.thumbnail?.objectUrl ||
|
||||||
|
getAbsoluteAttachmentPath(item.thumbnail?.path ?? ''),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (media.length === 1) {
|
const selectedMedia =
|
||||||
const props = {
|
media.find(item => attachment.path === item.path) || media[0];
|
||||||
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 selectedIndex = window._.findIndex(
|
this.showLightboxForMedia(selectedMedia, media, loop);
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
showContactModal(contactId: string) {
|
showContactModal(contactId: string) {
|
||||||
|
@ -3608,9 +3566,15 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
contentType: attachment.contentType,
|
contentType: attachment.contentType,
|
||||||
index,
|
index,
|
||||||
attachment,
|
attachment,
|
||||||
// this message is a valid structure, but doesn't work with ts
|
message: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
attachments: message.attachments || [],
|
||||||
message: message as any,
|
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),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
|
@ -96,7 +96,6 @@ import { ContactDetail } from './components/conversation/ContactDetail';
|
||||||
import { ContactModal } from './components/conversation/ContactModal';
|
import { ContactModal } from './components/conversation/ContactModal';
|
||||||
import { ErrorModal } from './components/ErrorModal';
|
import { ErrorModal } from './components/ErrorModal';
|
||||||
import { Lightbox } from './components/Lightbox';
|
import { Lightbox } from './components/Lightbox';
|
||||||
import { LightboxGallery } from './components/LightboxGallery';
|
|
||||||
import { MediaGallery } from './components/conversation/media-gallery/MediaGallery';
|
import { MediaGallery } from './components/conversation/media-gallery/MediaGallery';
|
||||||
import { MessageDetail } from './components/conversation/MessageDetail';
|
import { MessageDetail } from './components/conversation/MessageDetail';
|
||||||
import { ProgressModal } from './components/ProgressModal';
|
import { ProgressModal } from './components/ProgressModal';
|
||||||
|
@ -421,7 +420,6 @@ declare global {
|
||||||
DisappearingTimeDialog: typeof DisappearingTimeDialog;
|
DisappearingTimeDialog: typeof DisappearingTimeDialog;
|
||||||
ErrorModal: typeof ErrorModal;
|
ErrorModal: typeof ErrorModal;
|
||||||
Lightbox: typeof Lightbox;
|
Lightbox: typeof Lightbox;
|
||||||
LightboxGallery: typeof LightboxGallery;
|
|
||||||
MediaGallery: typeof MediaGallery;
|
MediaGallery: typeof MediaGallery;
|
||||||
MessageDetail: typeof MessageDetail;
|
MessageDetail: typeof MessageDetail;
|
||||||
ProgressModal: typeof ProgressModal;
|
ProgressModal: typeof ProgressModal;
|
||||||
|
|
Loading…
Reference in New Issue