diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js index ca88ee1be..90526241f 100644 --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -1,39 +1,145 @@ // @flow +import invariant from 'invariant'; import * as React from 'react'; +import { fetchableMediaURI } from 'lib/media/media-utils.js'; import type { EncryptedMediaType, MediaType, Dimensions, } from 'lib/types/media-types.js'; +import EncryptedMultimedia from './encrypted-multimedia.react.js'; +import LoadableVideo from './loadable-video.react.js'; +import { usePlaceholder } from './media-utils.js'; import FullScreenViewModal from '../modals/full-screen-view-modal.react.js'; type MediaInfo = | { +type: MediaType, +uri: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailURI: ?string, } | { +type: EncryptedMediaType, +blobURI: string, +encryptionKey: string, +dimensions: ?Dimensions, +thumbHash: ?string, +thumbnailBlobURI: ?string, +thumbnailEncryptionKey: ?string, }; type Props = { +media: MediaInfo, }; function MultimediaModal(props: Props): React.Node { - return ; + const { media } = props; + + const thumbHashEncryptionKey = + media.thumbnailEncryptionKey ?? media.encryptionKey; + + const placeholderImage = usePlaceholder( + media.thumbHash, + thumbHashEncryptionKey, + ); + + const [dimensions, setDimensions] = React.useState(null); + + const photo = React.useMemo(() => { + if (media.type !== 'photo') { + return null; + } + const uri = fetchableMediaURI(media.uri); + const style = { + backgroundImage: placeholderImage + ? `url(${placeholderImage})` + : undefined, + }; + + return ; + }, [media.type, media.uri, placeholderImage]); + + const video = React.useMemo(() => { + if (media.type !== 'video') { + return null; + } + + const uri = fetchableMediaURI(media.uri); + + const { thumbnailURI } = media; + invariant(thumbnailURI, 'video missing thumbnail'); + + return ( + + ); + }, [media, placeholderImage]); + + const encryptedMultimedia = React.useMemo(() => { + if (media.type !== 'encrypted_photo' && media.type !== 'encrypted_video') { + return null; + } + + const { + type, + blobURI, + encryptionKey, + thumbnailBlobURI, + thumbnailEncryptionKey, + } = media; + + const contentDimensions = dimensions ?? media.dimensions; + const elementStyle = contentDimensions + ? { + width: `${contentDimensions.width}px`, + height: `${contentDimensions.height}px`, + } + : undefined; + + return ( + + ); + }, [dimensions, media, placeholderImage]); + + const mediaModalItem = React.useMemo(() => { + if (media.type === 'photo') { + return photo; + } else if (media.type === 'video') { + return video; + } else { + return encryptedMultimedia; + } + }, [encryptedMultimedia, media.type, photo, video]); + + const multimediaModal = React.useMemo( + () => ( + + {mediaModalItem} + + ), + [dimensions, mediaModalItem], + ); + + return multimediaModal; } export default MultimediaModal; diff --git a/web/modals/full-screen-view-modal.css b/web/modals/full-screen-view-modal.css index dd96a1994..5d1cdbd57 100644 --- a/web/modals/full-screen-view-modal.css +++ b/web/modals/full-screen-view-modal.css @@ -1,60 +1,64 @@ -div.multimediaModalOverlay .loadingIndicator { +div.fullScreenModalOverlay .loadingIndicator { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto auto; width: 25px; height: 25px; } -div.multimediaModalOverlay { +div.fullScreenModalOverlay { position: fixed; left: 0; top: 0; z-index: 4; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); overflow: auto; padding: 10px; box-sizing: border-box; display: flex; justify-content: center; -webkit-app-region: no-drag; } -div.multimediaModalOverlay > .mediaContainer { + +div.fullScreenModalOverlay > .contentContainer { display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; } -div.multimediaModalOverlay > .mediaContainer:focus { + +div.fullScreenModalOverlay > .contentContainer:focus { outline: none; } -div.mediaContainer > img, -div.mediaContainer > video { + +div.contentContainer > img, +div.contentContainer > video { width: auto; height: auto; max-width: 100%; max-height: 100%; display: block; margin: auto; background-position: center; background-size: cover; background-repeat: no-repeat; } -svg.closeMultimediaModal { + +svg.closeFullScreenModal { position: absolute; cursor: pointer; top: 15px; right: 15px; color: white; border-radius: 50%; box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.5); background-color: rgba(34, 34, 34, 0.67); height: 36px; width: 36px; } diff --git a/web/modals/full-screen-view-modal.react.js b/web/modals/full-screen-view-modal.react.js index 3aca565db..0cf361dcf 100644 --- a/web/modals/full-screen-view-modal.react.js +++ b/web/modals/full-screen-view-modal.react.js @@ -1,208 +1,116 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { XCircle as XCircleIcon } from 'react-feather'; import { useModalContext } from 'lib/components/modal-provider.react.js'; -import { fetchableMediaURI } from 'lib/media/media-utils.js'; -import type { - EncryptedMediaType, - MediaType, - Dimensions, -} from 'lib/types/media-types.js'; +import type { SetState } from 'lib/types/hook-types.js'; +import type { Dimensions } from 'lib/types/media-types.js'; import css from './full-screen-view-modal.css'; -import EncryptedMultimedia from '../media/encrypted-multimedia.react.js'; -import LoadableVideo from '../media/loadable-video.react.js'; -import { usePlaceholder } from '../media/media-utils.js'; - -type MediaInfo = - | { - +type: MediaType, - +uri: string, - +dimensions: ?Dimensions, - +thumbHash: ?string, - +thumbnailURI: ?string, - } - | { - +type: EncryptedMediaType, - +blobURI: string, - +encryptionKey: string, - +dimensions: ?Dimensions, - +thumbHash: ?string, - +thumbnailBlobURI: ?string, - +thumbnailEncryptionKey: ?string, - }; type BaseProps = { - +media: MediaInfo, + +children: React.Node, + +contentDimensions: ?Dimensions, + +setContentDimensions: SetState, }; type Props = { ...BaseProps, +popModal: (modal: ?React.Node) => void, - +placeholderImage: ?string, -}; - -type State = { - +dimensions: ?Dimensions, }; -class MultimediaModal extends React.PureComponent { +class FullScreenViewModal extends React.PureComponent { overlay: ?HTMLDivElement; - constructor(props: Props) { - super(props); - this.state = { dimensions: null }; - } - componentDidMount() { invariant(this.overlay, 'overlay ref unset'); this.overlay.focus(); this.calculateMediaDimensions(); window.addEventListener('resize', this.calculateMediaDimensions); } componentWillUnmount() { window.removeEventListener('resize', this.calculateMediaDimensions); } render(): React.Node { - let mediaModalItem; - const { media, placeholderImage } = this.props; - const style = { - backgroundImage: placeholderImage - ? `url(${placeholderImage})` - : undefined, - }; - if (media.type === 'photo') { - const uri = fetchableMediaURI(media.uri); - mediaModalItem = ; - } else if (media.type === 'video') { - const uri = fetchableMediaURI(media.uri); - const { thumbnailURI } = media; - invariant(thumbnailURI, 'video missing thumbnail'); - mediaModalItem = ( - - ); - } else { - invariant( - media.type === 'encrypted_photo' || media.type === 'encrypted_video', - 'invalid media type', - ); - const { - type, - blobURI, - encryptionKey, - thumbnailBlobURI, - thumbnailEncryptionKey, - } = media; - const dimensions = this.state.dimensions ?? media.dimensions; - const elementStyle = dimensions - ? { - width: `${dimensions.width}px`, - height: `${dimensions.height}px`, - } - : undefined; - mediaModalItem = ( - - ); - } - return (
- {mediaModalItem} + {this.props.children}
); } overlayRef: (overlay: ?HTMLDivElement) => void = overlay => { this.overlay = overlay; }; onBackgroundClick: (event: SyntheticEvent) => void = event => { if (event.target === this.overlay) { this.props.popModal(); } }; onKeyDown: (event: SyntheticKeyboardEvent) => void = event => { if (event.key === 'Escape') { this.props.popModal(); } }; calculateMediaDimensions: () => void = () => { - if (!this.overlay || !this.props.media.dimensions) { + if (!this.overlay || !this.props.contentDimensions) { return; } const containerWidth = this.overlay.clientWidth; const containerHeight = this.overlay.clientHeight; const containerAspectRatio = containerWidth / containerHeight; const { width: mediaWidth, height: mediaHeight } = - this.props.media.dimensions; + this.props.contentDimensions; const mediaAspectRatio = mediaWidth / mediaHeight; let newWidth, newHeight; if (containerAspectRatio > mediaAspectRatio) { newWidth = Math.min(mediaWidth, containerHeight * mediaAspectRatio); newHeight = newWidth / mediaAspectRatio; } else { newHeight = Math.min(mediaHeight, containerWidth / mediaAspectRatio); newWidth = newHeight * mediaAspectRatio; } - this.setState({ - dimensions: { - width: newWidth, - height: newHeight, - }, + this.props.setContentDimensions({ + width: newWidth, + height: newHeight, }); }; } -function ConnectedMultiMediaModal(props: BaseProps): React.Node { +function ConnectedFullScreenViewModal(props: BaseProps): React.Node { const modalContext = useModalContext(); - const { thumbHash, encryptionKey, thumbnailEncryptionKey } = props.media; - const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; - const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); - - return ( - + + const fullScreenViewModal = React.useMemo( + () => , + [modalContext.popModal, props], ); + + return fullScreenViewModal; } -export default ConnectedMultiMediaModal; +export default ConnectedFullScreenViewModal;