diff --git a/lib/hooks/input-state-container-hooks.js b/lib/hooks/input-state-container-hooks.js --- a/lib/hooks/input-state-container-hooks.js +++ b/lib/hooks/input-state-container-hooks.js @@ -9,8 +9,18 @@ useSendMultimediaMessage, useSendTextMessage, } from '../actions/message-actions.js'; +import type { MediaMetadataUploadAction } from '../actions/upload-actions.js'; +import { + useMediaMetadataUpload, + updateMultimediaMessageMediaActionType, +} from '../actions/upload-actions.js'; +import { + encryptedMediaBlobURI, + encryptedVideoThumbnailBlobURI, +} from '../media/media-utils.js'; import { dmOperationSpecificationTypes } from '../shared/dm-ops/dm-op-utils.js'; import { useSendComposableDMOperation } from '../shared/dm-ops/process-dm-ops.js'; +import type { Media } from '../types/media-types.js'; import type { RawMultimediaMessageInfo, SendMessagePayload, @@ -18,11 +28,16 @@ import { getMediaMessageServerDBContentsFromMedia } from '../types/messages/media.js'; import type { RawTextMessageInfo } from '../types/messages/text.js'; import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import type { Dispatch } from '../types/redux-types.js'; import { thickThreadTypes, threadTypeIsThick, } from '../types/thread-types-enum.js'; -import { useSelector } from '../utils/redux-utils.js'; +import { + blobHashFromBlobServiceURI, + isBlobServiceURI, +} from '../utils/blob-service.js'; +import { useSelector, useDispatch } from '../utils/redux-utils.js'; function useInputStateContainerSendTextMessage(): ( messageInfo: RawTextMessageInfo, @@ -116,6 +131,9 @@ const sendComposableDMOperation = useSendComposableDMOperation(); const threadInfos = useSelector(state => state.threadStore.threadInfos); + const uploadMediaMetadata = useMediaMetadataUpload(); + const dispatch = useDispatch(); + return React.useCallback( async ( messageInfo: RawMultimediaMessageInfo, @@ -132,8 +150,13 @@ const isThickThread = threadInfo && threadTypeIsThick(threadInfo.type); if (!isThickThread && isLegacy) { + const { messageMedia } = await migrateMessageMediaToKeyserver( + messageInfo, + uploadMediaMetadata, + dispatch, + ); const mediaIDs = []; - for (const { id } of messageInfo.media) { + for (const { id } of messageMedia) { mediaIDs.push(id); } const result = await legacySendMultimediaMessage({ @@ -151,9 +174,13 @@ } if (!isThickThread && !isLegacy) { - const mediaMessageContents = getMediaMessageServerDBContentsFromMedia( - messageInfo.media, + const { messageMedia } = await migrateMessageMediaToKeyserver( + messageInfo, + uploadMediaMetadata, + dispatch, ); + const mediaMessageContents = + getMediaMessageServerDBContentsFromMedia(messageMedia); const result = await sendMultimediaMessage({ threadID: messageInfo.threadID, localID, @@ -206,14 +233,126 @@ }; }, [ + dispatch, legacySendMultimediaMessage, sendComposableDMOperation, sendMultimediaMessage, threadInfos, + uploadMediaMetadata, ], ); } +function mediaIDIsKeyserverID(mediaID: string): boolean { + return mediaID.indexOf('|') !== -1; +} + +type MediaIDUpdates = { +[string]: { id: string, thumbnailID?: string } }; + +async function migrateMessageMediaToKeyserver( + messageInfo: RawMultimediaMessageInfo, + uploadMediaMetadata: MediaMetadataUploadAction, + dispatch: Dispatch, +): Promise<{ + +messageMedia: $ReadOnlyArray, + +mediaIDUpdates: MediaIDUpdates, +}> { + const newMedia = []; + let mediaIDUpdates: MediaIDUpdates = {}; + for (const media of messageInfo.media) { + if ( + mediaIDIsKeyserverID(media.id) || + (media.type !== 'encrypted_photo' && media.type !== 'encrypted_video') + ) { + newMedia.push(media); + continue; + } + + const mediaURI = encryptedMediaBlobURI(media); + invariant( + isBlobServiceURI(mediaURI), + 'non-blob media had non-keyserver ID', + ); + + // This is only to determine server-side if media is photo or video. + // We can mock mime type to represent one of them. + const mimeType = + media.type === 'encrypted_photo' ? 'image/jpeg' : 'video/mp4'; + const blobHash = blobHashFromBlobServiceURI(mediaURI); + const { id } = await uploadMediaMetadata({ + keyserverOrThreadID: messageInfo.threadID, + uploadInput: { + blobHash, + mimeType, + dimensions: media.dimensions, + thumbHash: media.thumbHash, + encryptionKey: media.encryptionKey, + loop: media.loop, + }, + }); + + if (media.type !== 'encrypted_video') { + mediaIDUpdates = { + ...mediaIDUpdates, + [media.id]: { id }, + }; + newMedia.push({ + ...media, + id, + }); + continue; + } + + const thumbnailMediaURI = encryptedVideoThumbnailBlobURI(media); + invariant( + isBlobServiceURI(thumbnailMediaURI), + 'non-blob media had non-keyserver thumbnail ID', + ); + + const thumbnailBlobHash = blobHashFromBlobServiceURI(thumbnailMediaURI); + const { id: thumbnailID } = await uploadMediaMetadata({ + keyserverOrThreadID: messageInfo.threadID, + uploadInput: { + blobHash: thumbnailBlobHash, + mimeType: 'image/jpeg', + dimensions: media.dimensions, + thumbHash: media.thumbnailThumbHash, + encryptionKey: media.thumbnailEncryptionKey, + loop: false, + }, + }); + + mediaIDUpdates = { + ...mediaIDUpdates, + [media.id]: { id, thumbnailID }, + }; + newMedia.push({ + ...media, + id, + thumbnailID, + }); + } + + for (const [prevID, { id, thumbnailID }] of Object.entries(mediaIDUpdates)) { + dispatch({ + type: updateMultimediaMessageMediaActionType, + payload: { + messageID: messageInfo.localID, + currentMediaID: prevID, + mediaUpdate: { + id, + ...(thumbnailID ? { thumbnailID } : {}), + }, + }, + }); + } + + return { + messageMedia: newMedia, + mediaIDUpdates, + }; +} + export { useInputStateContainerSendTextMessage, useInputStateContainerSendMultimediaMessage,