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 @@ -1,60 +1,25 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; -import uuid from 'uuid'; import { useMediaMetadataReassignment } from './upload-hooks.js'; -import { - type ProcessHolders, - useProcessBlobHolders, -} from '../actions/holder-actions.js'; +import { useProcessBlobHolders } from '../actions/holder-actions.js'; import { useLegacySendMultimediaMessage, useSendMultimediaMessage, useSendTextMessage, } from '../actions/message-actions.js'; -import type { MediaMetadataReassignmentAction } from '../actions/upload-actions.js'; -import { updateMultimediaMessageMediaActionType } from '../actions/upload-actions.js'; -import { - encryptedMediaBlobURI, - encryptedVideoThumbnailBlobURI, -} from '../media/media-utils.js'; -import { dmOperationSpecificationTypes } from '../shared/dm-ops/dm-op-types.js'; import { useSendComposableDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { threadSpecs } from '../shared/threads/thread-specs.js'; -import type { BlobOperation } from '../types/holder-types.js'; -import type { - EncryptedImage, - EncryptedVideo, - Media, -} from '../types/media-types.js'; import type { RawMultimediaMessageInfo, SendMessagePayload, + SendMultimediaMessagePayload, } from '../types/message-types.js'; -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 { - blobHashFromBlobServiceURI, - isBlobServiceURI, -} from '../utils/blob-service.js'; -import { SendMessageError } from '../utils/errors.js'; import { useSelector, useDispatch } from '../utils/redux-utils.js'; -type MediaIDUpdatePayload = { +id: string, +thumbnailID?: string }; -type MediaIDUpdates = { +[string]: MediaIDUpdatePayload }; -export type SendMultimediaMessagePayload = { - +result: SendMessagePayload, - +mediaIDUpdates?: MediaIDUpdates, -}; - function useInputStateContainerSendTextMessage(): ( messageInfo: RawTextMessageInfo, threadInfo: ThreadInfo, @@ -107,118 +72,28 @@ sidebarCreation: boolean, isLegacy: boolean, ) => { - const { localID } = messageInfo; - invariant( - localID !== null && localID !== undefined, - 'localID should be set', - ); - const threadInfo = threadInfos[messageInfo.threadID]; - const isThickThread = threadInfo && threadTypeIsThick(threadInfo.type); - - if (!isThickThread && isLegacy) { - const { messageMedia, mediaIDUpdates } = - await migrateMessageMediaToKeyserver( - messageInfo, - reassignThickThreadMedia, - dispatch, - processHolders, - ); - const mediaIDs = []; - for (const { id } of messageMedia) { - mediaIDs.push(id); - } - const result = await legacySendMultimediaMessage({ - threadID: messageInfo.threadID, - localID, - mediaIDs, - sidebarCreation, - }); - return { - result: { - localID, - serverID: result.id, - threadID: messageInfo.threadID, - time: result.time, - }, - mediaIDUpdates, - }; - } - - if (!isThickThread && !isLegacy) { - const { messageMedia, mediaIDUpdates } = - await migrateMessageMediaToKeyserver( - messageInfo, - reassignThickThreadMedia, - dispatch, - processHolders, - ); - const mediaMessageContents = - getMediaMessageServerDBContentsFromMedia(messageMedia); - const result = await sendMultimediaMessage({ - threadID: messageInfo.threadID, - localID, - mediaMessageContents, + return threadSpecs[threadInfo.type].protocol.sendMultimediaMessage( + { + messageInfo, sidebarCreation, - }); - return { - result: { - localID, - serverID: result.id, - threadID: messageInfo.threadID, - time: result.time, - }, - mediaIDUpdates, - }; - } - - const messageID = uuid.v4(); - const time = Date.now(); - - const result = await sendComposableDMOperation({ - type: dmOperationSpecificationTypes.OUTBOUND, - op: { - type: 'send_multimedia_message', - threadID: threadInfo.id, - creatorID: messageInfo.creatorID, - time: Date.now(), - messageID, - media: messageInfo.media, - }, - recipients: { - type: 'all_thread_members', - threadID: - threadInfo.type === thickThreadTypes.THICK_SIDEBAR && - threadInfo.parentThreadID - ? threadInfo.parentThreadID - : threadInfo.id, + isLegacy, + threadInfo, }, - sendOnly: true, - composableMessageID: localID, - }); - - if (result.result === 'failure' && result.failedMessageIDs.length > 0) { - const error = new SendMessageError( - 'Failed to send message to all peers', - localID, - messageInfo.threadID, - ); - error.failedOutboundP2PMessageIDs = result.failedMessageIDs; - throw error; - } - return { - result: { - localID, - serverID: messageID, - threadID: messageInfo.threadID, - time, + { + sendKeyserverMultimediaMessage: sendMultimediaMessage, + legacyKeyserverSendMultimediaMessage: legacySendMultimediaMessage, + sendComposableDMOperation, + reassignThickThreadMedia, + processHolders, + dispatch, }, - }; + ); }, [ dispatch, - processHolders, legacySendMultimediaMessage, + processHolders, reassignThickThreadMedia, sendComposableDMOperation, sendMultimediaMessage, @@ -227,136 +102,6 @@ ); } -function mediaIDIsKeyserverID(mediaID: string): boolean { - return mediaID.indexOf('|') !== -1; -} - -async function migrateMessageMediaToKeyserver( - messageInfo: RawMultimediaMessageInfo, - reassignMediaMetadata: MediaMetadataReassignmentAction, - dispatch: Dispatch, - processHolders: ProcessHolders, -): Promise<{ - +messageMedia: $ReadOnlyArray, - +mediaIDUpdates: MediaIDUpdates, -}> { - const messageMedia = [], - holderActions: Array = []; - let mediaIDUpdates: MediaIDUpdates = {}; - - const processMediaChanges = ( - prevMediaID: string, - changes: { - ...MediaIDUpdatePayload, - +blobsToRemoveHolder: $ReadOnlyArray, - }, - ) => { - const { blobsToRemoveHolder, ...mediaUpdate } = changes; - const newHolderActions = blobsToRemoveHolder.map(blobHash => ({ - type: 'remove_holder', - blobHash, - })); - holderActions.push(...newHolderActions); - - mediaIDUpdates = { ...mediaIDUpdates, [prevMediaID]: mediaUpdate }; - dispatch({ - type: updateMultimediaMessageMediaActionType, - payload: { - messageID: messageInfo.localID, - currentMediaID: prevMediaID, - mediaUpdate, - }, - }); - }; - - const reassignmentPromises = messageInfo.media.map(async media => { - if ( - mediaIDIsKeyserverID(media.id) || - (media.type !== 'encrypted_photo' && media.type !== 'encrypted_video') - ) { - messageMedia.push(media); - return; - } - - const mediaURI = encryptedMediaBlobURI(media); - invariant( - isBlobServiceURI(mediaURI), - 'thick thread media should be blob-hosted', - ); - - // 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 mediaReassignmentPromise = reassignMediaMetadata({ - keyserverOrThreadID: messageInfo.threadID, - mediaMetadataInput: { - blobHash, - mimeType, - dimensions: media.dimensions, - thumbHash: media.thumbHash, - encryptionKey: media.encryptionKey, - loop: media.loop, - }, - }); - - if (media.type !== 'encrypted_video') { - const { id } = await mediaReassignmentPromise; - - const updatedMedia: EncryptedImage = { ...media, id }; - messageMedia.push(updatedMedia); - - const mediaChanges = { id, blobsToRemoveHolder: [blobHash] }; - processMediaChanges(media.id, mediaChanges); - - return; - } - - const thumbnailMediaURI = encryptedVideoThumbnailBlobURI(media); - invariant( - isBlobServiceURI(thumbnailMediaURI), - 'thick thread media thumbnail should be blob-hosted', - ); - - const thumbnailBlobHash = blobHashFromBlobServiceURI(thumbnailMediaURI); - const thumbnailReassignmentPromise = reassignMediaMetadata({ - keyserverOrThreadID: messageInfo.threadID, - mediaMetadataInput: { - blobHash: thumbnailBlobHash, - mimeType: 'image/jpeg', - dimensions: media.dimensions, - thumbHash: media.thumbnailThumbHash, - encryptionKey: media.thumbnailEncryptionKey, - loop: false, - }, - }); - - const [{ id }, { id: thumbnailID }] = await Promise.all([ - mediaReassignmentPromise, - thumbnailReassignmentPromise, - ]); - - const updatedMedia: EncryptedVideo = { ...media, id, thumbnailID }; - messageMedia.push(updatedMedia); - - const mediaChanges = { - id, - thumbnailID, - blobsToRemoveHolder: [blobHash, thumbnailBlobHash], - }; - processMediaChanges(media.id, mediaChanges); - }); - - await Promise.all(reassignmentPromises); - void processHolders(holderActions); - - return { - messageMedia, - mediaIDUpdates, - }; -} - export { useInputStateContainerSendTextMessage, useInputStateContainerSendMultimediaMessage, diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -8,8 +8,10 @@ import { dmOperationSpecificationTypes } from '../../dm-ops/dm-op-types.js'; import { type ProtocolSendTextMessageInput, + type SendMultimediaMessageUtils, type SendTextMessageUtils, type ThreadProtocol, + type ProtocolSendMultimediaMessageInput, } from '../thread-spec.js'; const dmThreadProtocol: ThreadProtocol = Object.freeze({ @@ -70,6 +72,61 @@ time, }; }, + + sendMultimediaMessage: async ( + message: ProtocolSendMultimediaMessageInput, + utils: SendMultimediaMessageUtils, + ) => { + const { messageInfo, threadInfo } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', + ); + + const messageID = uuid.v4(); + const time = Date.now(); + + const result = await utils.sendComposableDMOperation({ + type: dmOperationSpecificationTypes.OUTBOUND, + op: { + type: 'send_multimedia_message', + threadID: threadInfo.id, + creatorID: messageInfo.creatorID, + time: Date.now(), + messageID, + media: messageInfo.media, + }, + recipients: { + type: 'all_thread_members', + threadID: + threadInfo.type === thickThreadTypes.THICK_SIDEBAR && + threadInfo.parentThreadID + ? threadInfo.parentThreadID + : threadInfo.id, + }, + sendOnly: true, + composableMessageID: localID, + }); + + if (result.result === 'failure' && result.failedMessageIDs.length > 0) { + const error = new SendMessageError( + 'Failed to send message to all peers', + localID, + messageInfo.threadID, + ); + error.failedOutboundP2PMessageIDs = result.failedMessageIDs; + throw error; + } + return { + result: { + localID, + serverID: messageID, + threadID: messageInfo.threadID, + time, + }, + }; + }, }); export { dmThreadProtocol }; diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -2,10 +2,38 @@ import invariant from 'invariant'; +import type { ProcessHolders } from '../../../actions/holder-actions.js'; +import { + type MediaMetadataReassignmentAction, + updateMultimediaMessageMediaActionType, +} from '../../../actions/upload-actions.js'; +import { + encryptedMediaBlobURI, + encryptedVideoThumbnailBlobURI, +} from '../../../media/media-utils.js'; +import type { BlobOperation } from '../../../types/holder-types.js'; +import { + type EncryptedImage, + type EncryptedVideo, + type Media, +} from '../../../types/media-types.js'; +import { + type MediaIDUpdatePayload, + type MediaIDUpdates, + type RawMultimediaMessageInfo, +} from '../../../types/message-types.js'; +import { getMediaMessageServerDBContentsFromMedia } from '../../../types/messages/media.js'; +import type { Dispatch } from '../../../types/redux-types.js'; +import { + blobHashFromBlobServiceURI, + isBlobServiceURI, +} from '../../../utils/blob-service.js'; import { type ThreadProtocol, type ProtocolSendTextMessageInput, type SendTextMessageUtils, + type ProtocolSendMultimediaMessageInput, + type SendMultimediaMessageUtils, } from '../thread-spec.js'; const keyserverThreadProtocol: ThreadProtocol = Object.freeze({ @@ -32,6 +60,209 @@ time: result.time, }; }, + + sendMultimediaMessage: async ( + message: ProtocolSendMultimediaMessageInput, + utils: SendMultimediaMessageUtils, + ) => { + const { messageInfo, isLegacy, sidebarCreation } = message; + const { localID } = messageInfo; + invariant( + localID !== null && localID !== undefined, + 'localID should be set', + ); + + const { + reassignThickThreadMedia, + dispatch, + processHolders, + legacyKeyserverSendMultimediaMessage, + sendKeyserverMultimediaMessage, + } = utils; + if (isLegacy) { + const { messageMedia, mediaIDUpdates } = + await migrateMessageMediaToKeyserver( + messageInfo, + reassignThickThreadMedia, + dispatch, + processHolders, + ); + const mediaIDs = []; + for (const { id } of messageMedia) { + mediaIDs.push(id); + } + const result = await legacyKeyserverSendMultimediaMessage({ + threadID: messageInfo.threadID, + localID, + mediaIDs, + sidebarCreation, + }); + return { + result: { + localID, + serverID: result.id, + threadID: messageInfo.threadID, + time: result.time, + }, + mediaIDUpdates, + }; + } + + const { messageMedia, mediaIDUpdates } = + await migrateMessageMediaToKeyserver( + messageInfo, + reassignThickThreadMedia, + dispatch, + processHolders, + ); + const mediaMessageContents = + getMediaMessageServerDBContentsFromMedia(messageMedia); + const result = await sendKeyserverMultimediaMessage({ + threadID: messageInfo.threadID, + localID, + mediaMessageContents, + sidebarCreation, + }); + return { + result: { + localID, + serverID: result.id, + threadID: messageInfo.threadID, + time: result.time, + }, + mediaIDUpdates, + }; + }, }); +function mediaIDIsKeyserverID(mediaID: string): boolean { + return mediaID.indexOf('|') !== -1; +} + +async function migrateMessageMediaToKeyserver( + messageInfo: RawMultimediaMessageInfo, + reassignMediaMetadata: MediaMetadataReassignmentAction, + dispatch: Dispatch, + processHolders: ProcessHolders, +): Promise<{ + +messageMedia: $ReadOnlyArray, + +mediaIDUpdates: MediaIDUpdates, +}> { + const messageMedia = [], + holderActions: Array = []; + let mediaIDUpdates: MediaIDUpdates = {}; + + const processMediaChanges = ( + prevMediaID: string, + changes: { + ...MediaIDUpdatePayload, + +blobsToRemoveHolder: $ReadOnlyArray, + }, + ) => { + const { blobsToRemoveHolder, ...mediaUpdate } = changes; + const newHolderActions = blobsToRemoveHolder.map(blobHash => ({ + type: 'remove_holder', + blobHash, + })); + holderActions.push(...newHolderActions); + + mediaIDUpdates = { ...mediaIDUpdates, [prevMediaID]: mediaUpdate }; + dispatch({ + type: updateMultimediaMessageMediaActionType, + payload: { + messageID: messageInfo.localID, + currentMediaID: prevMediaID, + mediaUpdate, + }, + }); + }; + + const reassignmentPromises = messageInfo.media.map(async media => { + if ( + mediaIDIsKeyserverID(media.id) || + (media.type !== 'encrypted_photo' && media.type !== 'encrypted_video') + ) { + messageMedia.push(media); + return; + } + + const mediaURI = encryptedMediaBlobURI(media); + invariant( + isBlobServiceURI(mediaURI), + 'thick thread media should be blob-hosted', + ); + + // 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 mediaReassignmentPromise = reassignMediaMetadata({ + keyserverOrThreadID: messageInfo.threadID, + mediaMetadataInput: { + blobHash, + mimeType, + dimensions: media.dimensions, + thumbHash: media.thumbHash, + encryptionKey: media.encryptionKey, + loop: media.loop, + }, + }); + + if (media.type !== 'encrypted_video') { + const { id } = await mediaReassignmentPromise; + + const updatedMedia: EncryptedImage = { ...media, id }; + messageMedia.push(updatedMedia); + + const mediaChanges = { id, blobsToRemoveHolder: [blobHash] }; + processMediaChanges(media.id, mediaChanges); + + return; + } + + const thumbnailMediaURI = encryptedVideoThumbnailBlobURI(media); + invariant( + isBlobServiceURI(thumbnailMediaURI), + 'thick thread media thumbnail should be blob-hosted', + ); + + const thumbnailBlobHash = blobHashFromBlobServiceURI(thumbnailMediaURI); + const thumbnailReassignmentPromise = reassignMediaMetadata({ + keyserverOrThreadID: messageInfo.threadID, + mediaMetadataInput: { + blobHash: thumbnailBlobHash, + mimeType: 'image/jpeg', + dimensions: media.dimensions, + thumbHash: media.thumbnailThumbHash, + encryptionKey: media.thumbnailEncryptionKey, + loop: false, + }, + }); + + const [{ id }, { id: thumbnailID }] = await Promise.all([ + mediaReassignmentPromise, + thumbnailReassignmentPromise, + ]); + + const updatedMedia: EncryptedVideo = { ...media, id, thumbnailID }; + messageMedia.push(updatedMedia); + + const mediaChanges = { + id, + thumbnailID, + blobsToRemoveHolder: [blobHash, thumbnailBlobHash], + }; + processMediaChanges(media.id, mediaChanges); + }); + + await Promise.all(reassignmentPromises); + void processHolders(holderActions); + + return { + messageMedia, + mediaIDUpdates, + }; +} + export { keyserverThreadProtocol }; diff --git a/lib/shared/threads/thread-spec.js b/lib/shared/threads/thread-spec.js --- a/lib/shared/threads/thread-spec.js +++ b/lib/shared/threads/thread-spec.js @@ -1,13 +1,25 @@ // @flow -import type { SendTextMessageInput } from '../../actions/message-actions'; +import { type ProcessHolders } from '../../actions/holder-actions.js'; +import { + type LegacySendMultimediaMessageInput, + type SendMultimediaMessageInput, + type SendTextMessageInput, +} from '../../actions/message-actions.js'; +import { type MediaMetadataReassignmentAction } from '../../actions/upload-actions.js'; import type { ProcessOutboundP2PMessagesResult } from '../../tunnelbroker/peer-to-peer-context.js'; import type { SendMessageResult, SendMessagePayload, -} from '../../types/message-types'; + RawMultimediaMessageInfo, + SendMultimediaMessagePayload, +} from '../../types/message-types.js'; import type { RawTextMessageInfo } from '../../types/messages/text.js'; -import type { ThreadInfo } from '../../types/minimally-encoded-thread-permissions-types.js'; +import type { + RawThreadInfo, + ThreadInfo, +} from '../../types/minimally-encoded-thread-permissions-types.js'; +import type { Dispatch } from '../../types/redux-types.js'; import type { OutboundComposableDMOperationSpecification } from '../dm-ops/dm-op-types.js'; export type ThreadTrait = @@ -29,11 +41,30 @@ +sendComposableDMOperation: OutboundComposableDMOperationSpecification => Promise, }; +export type ProtocolSendMultimediaMessageInput = { + +messageInfo: RawMultimediaMessageInfo, + +sidebarCreation: boolean, + +isLegacy: boolean, + +threadInfo: RawThreadInfo, +}; +export type SendMultimediaMessageUtils = { + +sendKeyserverMultimediaMessage: SendMultimediaMessageInput => Promise, + +legacyKeyserverSendMultimediaMessage: LegacySendMultimediaMessageInput => Promise, + +sendComposableDMOperation: OutboundComposableDMOperationSpecification => Promise, + +reassignThickThreadMedia: MediaMetadataReassignmentAction, + +processHolders: ProcessHolders, + +dispatch: Dispatch, +}; + export type ThreadProtocol = { +sendTextMessage: ( message: ProtocolSendTextMessageInput, utils: SendTextMessageUtils, ) => Promise, + +sendMultimediaMessage: ( + message: ProtocolSendMultimediaMessageInput, + utils: SendMultimediaMessageUtils, + ) => Promise, }; export type ThreadSpec = { diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -631,6 +631,12 @@ +threadID: string, +time: number, }; +export type MediaIDUpdatePayload = { +id: string, +thumbnailID?: string }; +export type MediaIDUpdates = { +[string]: MediaIDUpdatePayload }; +export type SendMultimediaMessagePayload = { + +result: SendMessagePayload, + +mediaIDUpdates?: MediaIDUpdates, +}; export type SendTextMessageRequest = { +threadID: string, diff --git a/native/input/input-state-container.react.js b/native/input/input-state-container.react.js --- a/native/input/input-state-container.react.js +++ b/native/input/input-state-container.react.js @@ -20,7 +20,6 @@ } from 'lib/actions/upload-actions.js'; import { useInvalidCSATLogOut } from 'lib/actions/user-actions.js'; import { - type SendMultimediaMessagePayload, useInputStateContainerSendMultimediaMessage, useInputStateContainerSendTextMessage, } from 'lib/hooks/input-state-container-hooks.js'; @@ -66,6 +65,7 @@ type RawMessageInfo, type RawMultimediaMessageInfo, type SendMessagePayload, + type SendMultimediaMessagePayload, } from 'lib/types/message-types.js'; import type { RawImagesMessageInfo } from 'lib/types/messages/images.js'; import type { RawMediaMessageInfo } from 'lib/types/messages/media.js'; diff --git a/web/input/input-state-container.react.js b/web/input/input-state-container.react.js --- a/web/input/input-state-container.react.js +++ b/web/input/input-state-container.react.js @@ -28,7 +28,6 @@ useModalContext, } from 'lib/components/modal-provider.react.js'; import { - type SendMultimediaMessagePayload, useInputStateContainerSendMultimediaMessage, useInputStateContainerSendTextMessage, } from 'lib/hooks/input-state-container-hooks.js'; @@ -68,6 +67,7 @@ type RawMessageInfo, type RawMultimediaMessageInfo, type SendMessagePayload, + type SendMultimediaMessagePayload, } from 'lib/types/message-types.js'; import type { RawImagesMessageInfo } from 'lib/types/messages/images.js'; import type { RawMediaMessageInfo } from 'lib/types/messages/media.js';