diff --git a/native/media/image-modal.react.js b/native/media/image-modal.react.js --- a/native/media/image-modal.react.js +++ b/native/media/image-modal.react.js @@ -1,7 +1,6 @@ // @flow import Clipboard from '@react-native-clipboard/clipboard'; -import invariant from 'invariant'; import * as React from 'react'; import type { MediaInfo, Dimensions } from 'lib/types/media-types.js'; @@ -42,15 +41,10 @@ const intentionalSaveMedia = useIntentionalSaveMedia(); const onPressSave = React.useCallback(() => { - invariant( - mediaInfo.type === 'photo' || mediaInfo.type === 'video', - 'saving media of type ' + mediaInfo.type + ' is not supported', - ); - - const { id: uploadID, uri } = mediaInfo; + const uploadID = mediaInfo.id; const { id: messageServerID, localID: messageLocalID } = item.messageInfo; const ids = { uploadID, messageServerID, messageLocalID }; - return intentionalSaveMedia(uri, ids); + return intentionalSaveMedia(mediaInfo, ids); }, [intentionalSaveMedia, item.messageInfo, mediaInfo]); const onPressCopy = React.useCallback(() => { diff --git a/native/media/save-media.js b/native/media/save-media.js --- a/native/media/save-media.js +++ b/native/media/save-media.js @@ -13,6 +13,7 @@ MediaMissionStep, MediaMissionResult, MediaMissionFailure, + MediaInfo, } from 'lib/types/media-types.js'; import { reportTypes, @@ -27,6 +28,7 @@ } from 'lib/utils/report-utils.js'; import { fetchBlob } from './blob-utils.js'; +import { decryptMedia } from './encryption-utils.js'; import { fetchAssetInfo, fetchFileInfo, @@ -43,7 +45,7 @@ import { requestAndroidPermission } from '../utils/android-permissions.js'; export type IntentionalSaveMedia = ( - uri: string, + mediaInfo: MediaInfo, ids: { uploadID: string, messageServerID: ?string, @@ -56,7 +58,7 @@ const mediaReportsEnabled = useIsReportEnabled('mediaReports'); return React.useCallback( async ( - uri: string, + mediaInfo: MediaInfo, ids: { uploadID: string, messageServerID: ?string, @@ -64,11 +66,19 @@ }, ) => { const start = Date.now(); + const { uri: mediaURI, blobURI, holder, encryptionKey } = mediaInfo; + const uri = mediaURI ?? blobURI ?? holder; + invariant(uri, 'mediaInfo should have a uri or a blobURI'); + const steps: Array = [ { step: 'save_media', uri, time: start }, ]; - const { resultPromise, reportPromise } = saveMedia(uri, 'request'); + const { resultPromise, reportPromise } = saveMedia( + uri, + encryptionKey, + 'request', + ); const result = await resultPromise; const userTime = Date.now() - start; @@ -130,6 +140,7 @@ function saveMedia( uri: string, + encryptionKey?: ?string, permissions?: Permissions = 'check', ): { resultPromise: Promise, @@ -142,7 +153,12 @@ } }; - const reportPromise = innerSaveMedia(uri, permissions, sendResult); + const reportPromise = innerSaveMedia( + uri, + encryptionKey, + permissions, + sendResult, + ); const resultPromise = new Promise(resolve => { resolveResult = resolve; }); @@ -152,13 +168,14 @@ async function innerSaveMedia( uri: string, + encryptionKey?: ?string, permissions: Permissions, sendResult: (result: MediaMissionResult) => void, ): Promise<$ReadOnlyArray> { if (Platform.OS === 'android') { - return await saveMediaAndroid(uri, permissions, sendResult); + return await saveMediaAndroid(uri, encryptionKey, permissions, sendResult); } else if (Platform.OS === 'ios') { - return await saveMediaIOS(uri, sendResult); + return await saveMediaIOS(uri, encryptionKey, sendResult); } else { sendResult({ success: false, reason: 'save_unsupported' }); return []; @@ -172,6 +189,7 @@ // Pictures directory, and then trigger the media scanner to pick it up async function saveMediaAndroid( inputURI: string, + encryptionKey?: ?string, permissions: Permissions, sendResult: (result: MediaMissionResult) => void, ): Promise<$ReadOnlyArray> { @@ -221,7 +239,11 @@ promises.push( (async () => { const { result: tempSaveResult, steps: tempSaveSteps } = - await saveRemoteMediaToDisk(uri, temporaryDirectoryPath); + await saveRemoteMediaToDisk( + uri, + encryptionKey, + temporaryDirectoryPath, + ); steps.push(...tempSaveSteps); if (!tempSaveResult.success) { success = false; @@ -277,6 +299,7 @@ // On iOS, we save the media to the camera roll async function saveMediaIOS( inputURI: string, + encryptionKey?: ?string, sendResult: (result: MediaMissionResult) => void, ): Promise<$ReadOnlyArray> { const steps: Array = []; @@ -285,7 +308,7 @@ let tempFile; if (uri.startsWith('http')) { const { result: tempSaveResult, steps: tempSaveSteps } = - await saveRemoteMediaToDisk(uri, temporaryDirectoryPath); + await saveRemoteMediaToDisk(uri, encryptionKey, temporaryDirectoryPath); steps.push(...tempSaveSteps); if (!tempSaveResult.success) { sendResult(tempSaveResult); @@ -348,9 +371,54 @@ async function saveRemoteMediaToDisk( inputURI: string, + encryptionKey?: ?string, directory: string, // should end with a / ): Promise { const steps: Array = []; + if (encryptionKey) { + const { steps: decryptionSteps, result: decryptionResult } = + await decryptMedia(inputURI, encryptionKey, { + destination: 'file', + destinationDirectory: directory, + }); + steps.push(...decryptionSteps); + if (!decryptionResult.success) { + return { result: decryptionResult, steps }; + } + const { uri } = decryptionResult; + const path = pathFromURI(uri); + if (!path) { + return { + result: { success: false, reason: 'resolve_failed', uri }, + steps, + }; + } + + const { steps: fetchFileInfoSteps, result: fetchFileInfoResult } = + await fetchFileInfo(uri, undefined, { + mime: true, + }); + steps.push(...fetchFileInfoSteps); + if (!fetchFileInfoResult.success) { + return { result: fetchFileInfoResult, steps }; + } + const { mime } = fetchFileInfoResult; + if (!mime) { + return { + steps, + result: { + success: false, + reason: 'media_type_fetch_failed', + detectedMIME: mime, + }, + }; + } + + return { + result: { success: true, path, mime }, + steps, + }; + } const { result: fetchBlobResult, steps: fetchBlobSteps } = await fetchBlob(inputURI);