diff --git a/native/avatars/avatar-hooks.js b/native/avatars/avatar-hooks.js --- a/native/avatars/avatar-hooks.js +++ b/native/avatars/avatar-hooks.js @@ -10,7 +10,10 @@ import { EditThreadAvatarContext } from 'lib/components/base-edit-thread-avatar-provider.react.js'; import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; -import { useBlobServiceUpload } from 'lib/hooks/upload-hooks.js'; +import { + useBlobServiceUpload, + usePlaintextMediaUpload, +} from 'lib/hooks/upload-hooks.js'; import { extensionFromFilename, filenameFromPathOrURI, @@ -37,7 +40,10 @@ import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; -import { blobServiceUploadHandler } from '../utils/blob-service-upload.js'; +import { + blobServiceUploadHandler, + plaintextMediaUploadHandler, +} from '../utils/blob-service-upload.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; function displayAvatarUpdateFailureAlert(): void { @@ -52,10 +58,47 @@ function useUploadProcessedMedia(): ( media: MediaResult, metadataUploadLocation: 'keyserver' | 'none', + supportsEncryption?: boolean, ) => Promise { const callBlobServiceUpload = useBlobServiceUpload(); + const callPlaintextMediaUpload = usePlaintextMediaUpload(); + return React.useCallback( - async (processedMedia, metadataUploadLocation) => { + async ( + processedMedia, + metadataUploadLocation, + supportsEncryption = true, + ) => { + if (!supportsEncryption) { + invariant(processedMedia.success, 'processedMedia must be successful'); + invariant( + processedMedia.mediaType === 'photo', + 'Avatar must be a photo', + ); + + const uploadResult = await callPlaintextMediaUpload({ + mediaInput: { + uploadInput: { + type: 'uri', + uri: processedMedia.uploadURI, + filename: processedMedia.filename, + mimeType: processedMedia.mime, + }, + dimensions: processedMedia.dimensions, + loop: false, + thumbHash: null, + }, + callbacks: { plaintextMediaUploadHandler }, + }); + + return { + type: 'non_keyserver_image', + blobURI: uploadResult.uri, + thumbHash: null, + encryptionKey: '', + }; + } + const { result: encryptionResult } = await encryptMedia(processedMedia); if (!encryptionResult.success) { throw new Error('Avatar media encryption failed.'); @@ -107,7 +150,7 @@ } return { type: 'encrypted_image', uploadID: id }; }, - [callBlobServiceUpload], + [callBlobServiceUpload, callPlaintextMediaUpload], ); } @@ -178,12 +221,17 @@ ): ( selection: NativeMediaSelection, metadataUploadLocation: 'keyserver' | 'none', + supportsEncryption?: boolean, ) => Promise { const processSelectedMedia = useProcessSelectedMedia(); const uploadProcessedMedia = useUploadProcessedMedia(); return React.useCallback( - async (selection: NativeMediaSelection, metadataUploadLocation) => { + async ( + selection: NativeMediaSelection, + metadataUploadLocation, + supportsEncryption = true, + ) => { setProcessingOrUploadInProgress?.(true); const urisToBeDisposed: Set = new Set([selection.uri]); @@ -218,6 +266,7 @@ uploadedMedia = await uploadProcessedMedia( processedMedia, metadataUploadLocation, + supportsEncryption, ); urisToBeDisposed.forEach(filesystem.unlink); setProcessingOrUploadInProgress?.(false); @@ -400,13 +449,14 @@ selection: NativeMediaSelection, threadInfo: ThreadInfo | RawThreadInfo, ): Promise => { - const metadataUploadLocation = threadSpecs[threadInfo.type].protocol() - .uploadMultimediaMetadataToKeyserver - ? 'keyserver' - : 'none'; + const protocol = threadSpecs[threadInfo.type].protocol(); + const metadataUploadLocation = + protocol.uploadMultimediaMetadataToKeyserver ? 'keyserver' : 'none'; + const supportsEncryption = protocol.supportsEncryptedMultimedia; const imageAvatarUpdateRequest = await uploadSelectedMedia( selection, metadataUploadLocation, + supportsEncryption, ); if (!imageAvatarUpdateRequest) { return; diff --git a/web/avatars/avatar-hooks.react.js b/web/avatars/avatar-hooks.react.js --- a/web/avatars/avatar-hooks.react.js +++ b/web/avatars/avatar-hooks.react.js @@ -2,7 +2,10 @@ import * as React from 'react'; -import { useBlobServiceUpload } from 'lib/hooks/upload-hooks.js'; +import { + useBlobServiceUpload, + usePlaintextMediaUpload, +} from 'lib/hooks/upload-hooks.js'; import type { UpdateUserAvatarRequest } from 'lib/types/avatar-types.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; @@ -12,14 +15,18 @@ type AvatarMediaUploadOptions = { +uploadMetadataToKeyserver?: boolean, + +supportsEncryption?: boolean, }; function useUploadAvatarMedia( options: AvatarMediaUploadOptions = {}, ): File => Promise { - const { uploadMetadataToKeyserver = true } = options; + const { uploadMetadataToKeyserver = true, supportsEncryption = true } = + options; const callBlobServiceUpload = useBlobServiceUpload(); + const callPlaintextMediaUpload = usePlaintextMediaUpload(); + const uploadAvatarMedia = React.useCallback( async (file: File): Promise => { const validatedFile = await validateFile(file); @@ -29,6 +36,25 @@ } const { file: fixedFile, dimensions } = result; + if (!supportsEncryption) { + const uploadResult = await callPlaintextMediaUpload({ + mediaInput: { + uploadInput: { type: 'file', file: fixedFile }, + dimensions, + loop: false, + thumbHash: null, + }, + callbacks: {}, + }); + + return { + type: 'non_keyserver_image', + blobURI: uploadResult.uri, + thumbHash: null, + encryptionKey: '', + }; + } + const encryptionResponse = await encryptFile(fixedFile); const { result: encryptionResult } = encryptionResponse; if (!encryptionResult.success) { @@ -77,7 +103,12 @@ encryptionKey, }; }, - [callBlobServiceUpload, uploadMetadataToKeyserver], + [ + callBlobServiceUpload, + callPlaintextMediaUpload, + uploadMetadataToKeyserver, + supportsEncryption, + ], ); return uploadAvatarMedia; } diff --git a/web/avatars/edit-thread-avatar-menu.react.js b/web/avatars/edit-thread-avatar-menu.react.js --- a/web/avatars/edit-thread-avatar-menu.react.js +++ b/web/avatars/edit-thread-avatar-menu.react.js @@ -75,6 +75,8 @@ uploadMetadataToKeyserver: threadSpecs[threadInfo.type].protocol() .uploadMultimediaMetadataToKeyserver, + supportsEncryption: + threadSpecs[threadInfo.type].protocol().supportsEncryptedMultimedia, }); const onImageSelected = React.useCallback( async (event: SyntheticEvent) => {