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
@@ -87,6 +87,7 @@
   type TypeaheadState,
   InputStateContext,
 } from './input-state.js';
+import { encryptFile } from '../media/encryption-utils.js';
 import { validateFile, preloadImage } from '../media/media-utils.js';
 import InvalidUploadModal from '../modals/chat/invalid-upload.react.js';
 import { updateNavInfoActionType } from '../redux/action-types.js';
@@ -163,6 +164,8 @@
   };
   replyCallbacks: Array<(message: string) => void> = [];
   pendingThreadCreations = new Map<string, Promise<string>>();
+  // TODO: we want to send encrypted media if thread is in the Comm community
+  sendEncryptedMedia: boolean = false;
 
   // When the user sends a multimedia message that triggers the creation of a
   // sidebar, the sidebar gets created right away, but the message needs to wait
@@ -284,7 +287,7 @@
       const creatorID = this.props.viewerID;
       invariant(creatorID, 'need viewer ID in order to send a message');
       const media = uploads.map(
-        ({ localID, serverID, uri, mediaType, dimensions }) => {
+        ({ localID, serverID, uri, mediaType, dimensions, encryptionKey }) => {
           // We can get into this state where dimensions are null if the user is
           // uploading a file type that the browser can't render. In that case
           // we fake the dimensions here while we wait for the server to tell us
@@ -294,23 +297,42 @@
           // native), 0,0 is probably a good default.
           const shimmedDimensions = dimensions ?? { height: 0, width: 0 };
           invariant(
-            mediaType === 'photo',
+            mediaType === 'photo' || mediaType === 'encrypted_photo',
             "web InputStateContainer can't handle video",
           );
+          if (
+            mediaType !== 'encrypted_photo' &&
+            mediaType !== 'encrypted_video'
+          ) {
+            return {
+              id: serverID ? serverID : localID,
+              uri,
+              type: 'photo',
+              dimensions: shimmedDimensions,
+            };
+          }
+          invariant(
+            encryptionKey,
+            'encrypted media must have an encryption key',
+          );
           return {
             id: serverID ? serverID : localID,
-            uri,
-            type: 'photo',
+            holder: uri,
+            type: 'encrypted_photo',
+            encryptionKey,
             dimensions: shimmedDimensions,
           };
         },
       );
-      const messageInfo = createMediaMessageInfo({
-        localID: messageID,
-        threadID,
-        creatorID,
-        media,
-      });
+      const messageInfo = createMediaMessageInfo(
+        {
+          localID: messageID,
+          threadID,
+          creatorID,
+          media,
+        },
+        { forceMultimediaMessageType: this.sendEncryptedMedia },
+      );
       newMessageInfos.set(messageID, messageInfo);
     }
 
@@ -676,8 +698,32 @@
     if (!result.success) {
       return { steps, result };
     }
-
     const { uri, file: fixedFile, mediaType, dimensions } = result;
+
+    let encryptionResult;
+    if (this.sendEncryptedMedia) {
+      let encryptionResponse;
+      const encryptionStart = Date.now();
+      try {
+        encryptionResponse = await encryptFile(fixedFile);
+      } catch (e) {
+        return {
+          steps,
+          result: {
+            success: false,
+            reason: 'encryption_exception',
+            time: Date.now() - encryptionStart,
+            exceptionMessage: getMessageForException(e),
+          },
+        };
+      }
+      steps.push(...encryptionResponse.steps);
+      encryptionResult = encryptionResponse.result;
+    }
+    if (encryptionResult && !encryptionResult.success) {
+      return { steps, result: encryptionResult };
+    }
+
     return {
       steps,
       result: {
@@ -687,13 +733,15 @@
           serverID: null,
           messageID: null,
           failed: false,
-          file: fixedFile,
-          mediaType,
+          file: encryptionResult ? encryptionResult.file : fixedFile,
+          mediaType: encryptionResult ? 'encrypted_photo' : mediaType,
           dimensions,
-          uri,
+          uri: encryptionResult ? encryptionResult.uri : uri,
           loop: false,
           uriIsReal: false,
-          encryptionKey: null,
+          encryptionKey: encryptionResult
+            ? encryptionResult.encryptionKey
+            : null,
           progressPercent: 0,
           abort: null,
           steps,
@@ -713,7 +761,12 @@
   }
 
   async uploadFile(threadID: string, upload: PendingMultimediaUpload) {
-    const { selectTime, localID } = upload;
+    const { selectTime, localID, encryptionKey } = upload;
+    const isEncrypted =
+      !!encryptionKey &&
+      (upload.mediaType === 'encrypted_photo' ||
+        upload.mediaType === 'encrypted_video');
+
     const steps = [...upload.steps];
     let userTime;
 
@@ -741,9 +794,13 @@
     let uploadResult, uploadExceptionMessage;
     const uploadStart = Date.now();
     try {
+      let uploadExtras = { ...upload.dimensions, loop: false };
+      if (encryptionKey) {
+        uploadExtras = { ...uploadExtras, encryptionKey };
+      }
       uploadResult = await this.props.uploadMultimedia(
         upload.file,
-        { ...upload.dimensions, loop: false },
+        uploadExtras,
         {
           onProgress: (percent: number) =>
             this.setProgress(threadID, localID, percent),
@@ -776,6 +833,7 @@
       return;
     }
     const result = uploadResult;
+    const outputMediaType = isEncrypted ? 'encrypted_photo' : result.mediaType;
 
     const successThreadID = this.getRealizedOrPendingThreadID(threadID);
     const uploadAfterSuccess =
@@ -822,8 +880,11 @@
       };
     });
 
-    const { steps: preloadSteps } = await preloadImage(result.uri);
-    steps.push(...preloadSteps);
+    // we cannot preload encrypted media this way, we don't have cache
+    if (!encryptionKey) {
+      const { steps: preloadSteps } = await preloadImage(result.uri);
+      steps.push(...preloadSteps);
+    }
     sendReport({ success: true });
 
     const preloadThreadID = this.getRealizedOrPendingThreadID(threadID);
@@ -836,6 +897,18 @@
     );
     if (uploadAfterPreload.messageID) {
       const { mediaType, uri, dimensions, loop } = result;
+      let mediaUpdate;
+      if (!isEncrypted) {
+        mediaUpdate = { type: mediaType, uri, dimensions, loop };
+      } else {
+        mediaUpdate = {
+          type: outputMediaType,
+          holder: uri,
+          encryptionKey,
+          dimensions,
+          loop,
+        };
+      }
       this.props.dispatch({
         type: updateMultimediaMessageMediaActionType,
         payload: {
@@ -843,7 +916,7 @@
           currentMediaID: uploadAfterPreload.serverID
             ? uploadAfterPreload.serverID
             : uploadAfterPreload.localID,
-          mediaUpdate: { type: mediaType, uri, dimensions, loop },
+          mediaUpdate,
         },
       });
     }
@@ -875,7 +948,7 @@
             [localID]: {
               ...currentUpload,
               uri: result.uri,
-              mediaType: result.mediaType,
+              mediaType: outputMediaType,
               dimensions: result.dimensions,
               uriIsReal: true,
               loop: result.loop,