diff --git a/web/chat/chat-input-bar.react.js b/web/chat/chat-input-bar.react.js --- a/web/chat/chat-input-bar.react.js +++ b/web/chat/chat-input-bar.react.js @@ -232,24 +232,28 @@ const { pendingUploads, cancelPendingUpload } = this.props.inputState; const multimediaPreviews = pendingUploads.map(pendingUpload => { - let mediaSource; - if ( - pendingUpload.mediaType !== 'encrypted_photo' && - pendingUpload.mediaType !== 'encrypted_video' - ) { + const { uri, mediaType, thumbHash, dimensions } = pendingUpload; + let mediaSource = { thumbHash, dimensions }; + if (mediaType !== 'encrypted_photo' && mediaType !== 'encrypted_video') { mediaSource = { - type: pendingUpload.mediaType, - uri: pendingUpload.uri, + ...mediaSource, + type: mediaType, + uri, + thumbnailURI: null, }; } else { + const { encryptionKey } = pendingUpload; invariant( - pendingUpload.encryptionKey, + encryptionKey, 'encryptionKey should be set for encrypted media', ); mediaSource = { - type: pendingUpload.mediaType, - holder: pendingUpload.uri, - encryptionKey: pendingUpload.encryptionKey, + ...mediaSource, + type: mediaType, + holder: uri, + encryptionKey, + thumbnailHolder: null, + thumbnailEncryptionKey: null, }; } return ( diff --git a/web/chat/multimedia-message.react.js b/web/chat/multimedia-message.react.js --- a/web/chat/multimedia-message.react.js +++ b/web/chat/multimedia-message.react.js @@ -39,17 +39,28 @@ const pendingUpload = pendingUploads ? pendingUploads.find(upload => upload.localID === singleMedia.id) : null; + const thumbHash = singleMedia.thumbHash ?? singleMedia.thumbnailThumbHash; let mediaSource; if (singleMedia.type === 'photo' || singleMedia.type === 'video') { - mediaSource = { - type: singleMedia.type, - uri: singleMedia.uri, - }; + const { type, uri, thumbnailURI, dimensions } = singleMedia; + mediaSource = { type, uri, thumbHash, thumbnailURI, dimensions }; } else { + const { + type, + holder, + encryptionKey, + thumbnailHolder, + thumbnailEncryptionKey, + dimensions, + } = singleMedia; mediaSource = { - type: singleMedia.type, - holder: singleMedia.holder, - encryptionKey: singleMedia.encryptionKey, + type, + holder, + encryptionKey, + thumbnailHolder, + thumbnailEncryptionKey, + dimensions, + thumbHash, }; } diff --git a/web/media/media.css b/web/media/media.css --- a/web/media/media.css +++ b/web/media/media.css @@ -16,6 +16,8 @@ } span.multimedia > .multimediaImage > img, span.multimedia > .multimediaImage > video { + /* this should be in sync with the MAX_THUMBNAIL_HEIGHT */ + /* in multimedia.react.js */ max-height: 200px; max-width: 100%; } 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 @@ -12,23 +12,40 @@ import { useModalContext } from 'lib/components/modal-provider.react.js'; import { fetchableMediaURI } from 'lib/media/media-utils.js'; -import type { MediaType, EncryptedMediaType } from 'lib/types/media-types.js'; +import type { + Dimensions, + EncryptedMediaType, + MediaType, +} 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'; import MultimediaModal from './multimedia-modal.react.js'; import Button from '../components/button.react.js'; import { type PendingMultimediaUpload } from '../input/input-state.js'; +// this should be in sync with the max-height value +// for span.multimedia > .multimediaImage in media.css +const MAX_THUMBNAIL_HEIGHT = 200; + type MediaSource = | { +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 Props = { @@ -81,7 +98,19 @@ const { pushModal } = useModalContext(); const handleClick = React.useCallback(() => { - pushModal(); + 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, mediaSource]); let progressIndicator, errorIndicator, removeButton; @@ -122,23 +151,60 @@ const imageContainerClasses = [css.multimediaImage, multimediaImageCSSClass]; imageContainerClasses.push(css.clickable); + const thumbHash = mediaSource.thumbHash ?? pendingUpload?.thumbHash; + const { encryptionKey, thumbnailEncryptionKey } = mediaSource; + const thumbHashEncryptionKey = thumbnailEncryptionKey ?? encryptionKey; + const placeholderImage = usePlaceholder(thumbHash, thumbHashEncryptionKey); + + const { dimensions } = mediaSource; + const elementStyle = React.useMemo(() => { + if (!dimensions) { + return undefined; + } + const { width, height } = dimensions; + // Resize the image to fit in max width while preserving aspect ratio + const calculatedWidth = + Math.min(MAX_THUMBNAIL_HEIGHT, height) * (width / height); + return { + background: placeholderImage + ? `center / cover url(${placeholderImage})` + : undefined, + width: `${calculatedWidth}px`, + // height is limited by the max-height style in media.css + height: `${height}px`, + }; + }, [dimensions, placeholderImage]); + // Media element is the actual image or video element (or encrypted version) let mediaElement; if (mediaSource.type === 'photo') { const uri = fetchableMediaURI(mediaSource.uri); - mediaElement = ; + mediaElement = ; } else if (mediaSource.type === 'video') { const uri = fetchableMediaURI(mediaSource.uri); + const { thumbnailURI } = mediaSource; + invariant(thumbnailURI, 'video missing thumbnail'); mediaElement = ( - + ); } else if ( mediaSource.type === 'encrypted_photo' || mediaSource.type === 'encrypted_video' ) { - mediaElement = ; + const { type, holder } = mediaSource; + invariant(encryptionKey, 'encryptionKey undefined for encrypted media'); + mediaElement = ( + + ); } // Media node is the container for the media element (button if photo)