diff --git a/web/media/media.css b/web/media/media.css --- a/web/media/media.css +++ b/web/media/media.css @@ -95,13 +95,24 @@ display: flex; justify-content: center; } -div.multimediaModalOverlay > img, -div.multimediaModalOverlay > video { - object-fit: scale-down; +div.multimediaModalOverlay > .mediaContainer { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; +} +div.mediaContainer > img, +div.mediaContainer > 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 { position: absolute; diff --git a/web/media/multimedia-modal.react.js b/web/media/multimedia-modal.react.js --- a/web/media/multimedia-modal.react.js +++ b/web/media/multimedia-modal.react.js @@ -6,20 +6,33 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import { fetchableMediaURI } from 'lib/media/media-utils.js'; -import type { EncryptedMediaType, MediaType } from 'lib/types/media-types.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 css from './media.css'; type MediaInfo = | { +type: MediaType, +uri: string, + +dimensions: ?Dimensions, + +thumbHash: ?string, + +thumbnailURI: ?string, } | { +type: EncryptedMediaType, +holder: string, +encryptionKey: string, + +dimensions: ?Dimensions, + +thumbHash: ?string, + +thumbnailHolder: ?string, + +thumbnailEncryptionKey: ?string, }; type BaseProps = { @@ -29,28 +42,53 @@ type Props = { ...BaseProps, +popModal: (modal: ?React.Node) => void, + +placeholderImage: ?string, }; -class MultimediaModal extends React.PureComponent { +type State = { + +dimensions: ?Dimensions, +}; + +class MultimediaModal 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 } = this.props; + const { media, placeholderImage } = this.props; + const style = { + backgroundImage: placeholderImage + ? `url(${placeholderImage})` + : undefined, + }; if (media.type === 'photo') { const uri = fetchableMediaURI(media.uri); - mediaModalItem = ; + mediaModalItem = ; } else if (media.type === 'video') { const uri = fetchableMediaURI(media.uri); + const { thumbnailURI } = media; + invariant(thumbnailURI, 'video missing thumbnail'); mediaModalItem = ( - + ); } else { invariant( @@ -75,7 +113,7 @@ tabIndex={0} onKeyDown={this.onKeyDown} > - {mediaModalItem} +
{mediaModalItem}
void = () => { + if (!this.overlay || !this.props.media.dimensions) { + return; + } + const containerWidth = this.overlay.clientWidth; + const containerHeight = this.overlay.clientHeight; + const containerAspectRatio = containerWidth / containerHeight; + + const { width: mediaWidth, height: mediaHeight } = + this.props.media.dimensions; + 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, + }, + }); + }; } function ConnectedMultiMediaModal(props: BaseProps): React.Node { const modalContext = useModalContext(); - - return ; + const { thumbHash, encryptionKey, thumbnailEncryptionKey } = props.media; + const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; + const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); + + return ( + + ); } export default ConnectedMultiMediaModal; diff --git a/web/media/multimedia.react.js b/web/media/multimedia.react.js --- a/web/media/multimedia.react.js +++ b/web/media/multimedia.react.js @@ -98,19 +98,7 @@ const { pushModal } = useModalContext(); const handleClick = React.useCallback(() => { - let media; - if ( - mediaSource.type === 'encrypted_photo' || - mediaSource.type === 'encrypted_video' - ) { - const { type, holder, encryptionKey } = mediaSource; - media = { type, holder, encryptionKey }; - } else { - const { type, uri } = mediaSource; - invariant(uri, 'uri is missing for media modal'); - media = { type, uri }; - } - pushModal(); + pushModal(); }, [pushModal, mediaSource]); let progressIndicator, errorIndicator, removeButton; diff --git a/web/modals/threads/gallery/thread-settings-media-gallery.react.js b/web/modals/threads/gallery/thread-settings-media-gallery.react.js --- a/web/modals/threads/gallery/thread-settings-media-gallery.react.js +++ b/web/modals/threads/gallery/thread-settings-media-gallery.react.js @@ -48,18 +48,33 @@ const onClick = React.useCallback( (media: Media) => { - // This branching is needed for Flow. - let mediaInfo; + const thumbHash = media.thumbnailThumbHash ?? media.thumbHash; + let mediaInfo = { + thumbHash, + dimensions: media.dimensions, + }; if (media.type === 'photo' || media.type === 'video') { + const { uri, thumbnailURI } = media; mediaInfo = { + ...mediaInfo, type: media.type, - uri: media.uri, + uri, + thumbnailURI, }; } else { + const { + holder, + encryptionKey, + thumbnailHolder, + thumbnailEncryptionKey, + } = media; mediaInfo = { + ...mediaInfo, type: media.type, - holder: media.holder, - encryptionKey: media.encryptionKey, + holder, + encryptionKey, + thumbnailHolder, + thumbnailEncryptionKey, }; } pushModal();