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)