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
@@ -7,11 +7,9 @@
 import _partition from 'lodash/fp/partition.js';
 import _sortBy from 'lodash/fp/sortBy.js';
 import _memoize from 'lodash/memoize.js';
-import _throttle from 'lodash/throttle.js';
 import * as React from 'react';
 import { useDispatch } from 'react-redux';
 import { createSelector } from 'reselect';
-import uuid from 'uuid';
 
 import {
   createLocalMessageActionType,
@@ -27,8 +25,10 @@
   uploadMediaMetadata,
   updateMultimediaMessageMediaActionType,
   deleteUpload,
+  blobServiceUpload,
   type MultimediaUploadCallbacks,
   type MultimediaUploadExtras,
+  type BlobServiceUploadAction,
 } from 'lib/actions/upload-actions.js';
 import {
   useModalContext,
@@ -60,7 +60,6 @@
   MediaMissionFailure,
   MediaMissionResult,
   MediaMission,
-  Dimensions,
 } from 'lib/types/media-types.js';
 import { messageTypes } from 'lib/types/message-types-enum.js';
 import {
@@ -86,17 +85,14 @@
   useServerCall,
   useDispatchActionPromise,
 } from 'lib/utils/action-utils.js';
-import { toBase64URL } from 'lib/utils/base64.js';
 import {
   makeBlobServiceEndpointURL,
   isBlobServiceURI,
   blobHashFromBlobServiceURI,
 } from 'lib/utils/blob-service.js';
-import type { CallServerEndpointOptions } from 'lib/utils/call-server-endpoint.js';
 import { getConfig } from 'lib/utils/config.js';
 import { getMessageForException, cloneError } from 'lib/utils/errors.js';
 import { generateReportID } from 'lib/utils/report-utils.js';
-import { handleHTTPResponseError } from 'lib/utils/services-utils.js';
 
 import {
   type PendingMultimediaUpload,
@@ -134,6 +130,7 @@
     extras: MultimediaUploadExtras,
     callbacks: MultimediaUploadCallbacks,
   ) => Promise<UploadMultimediaResult>,
+  +blobServiceUpload: BlobServiceUploadAction,
   +uploadMediaMetadata: (
     input: UploadMediaMetadataRequest,
   ) => Promise<UploadMultimediaResult>,
@@ -879,17 +876,20 @@
           encryptionKey && blobHash && dimensions,
           'incomplete encrypted upload',
         );
-        uploadResult = await this.blobServiceUpload(
-          {
-            file: upload.file,
+        uploadResult = await this.props.blobServiceUpload({
+          input: {
+            blobData: {
+              type: 'file',
+              file: upload.file,
+            },
             blobHash,
             encryptionKey,
             dimensions,
             loop: false,
-            ...(thumbHash ? { thumbHash } : undefined),
+            thumbHash,
           },
-          { ...callbacks },
-        );
+          callbacks,
+        });
       } else {
         let uploadExtras = {
           ...upload.dimensions,
@@ -1065,128 +1065,6 @@
     });
   }
 
-  async blobServiceUpload(
-    input: {
-      +file: File,
-      +blobHash: string,
-      +encryptionKey: string,
-      +dimensions: Dimensions,
-      +loop?: boolean,
-      +thumbHash?: string,
-    },
-    options?: ?CallServerEndpointOptions,
-  ): Promise<{
-    ...UploadMultimediaResult,
-    +blobHolder: string,
-  }> {
-    const blobHolder = uuid.v4();
-    const blobHash = toBase64URL(input.blobHash);
-
-    // 1. Assign new holder for blob with given blobHash
-    let blobAlreadyExists: boolean;
-    try {
-      const assignHolderEndpoint = blobService.httpEndpoints.ASSIGN_HOLDER;
-      const assignHolderResponse = await fetch(
-        makeBlobServiceEndpointURL(assignHolderEndpoint),
-        {
-          method: assignHolderEndpoint.method,
-          body: JSON.stringify({
-            holder: blobHolder,
-            blob_hash: blobHash,
-          }),
-          headers: {
-            'content-type': 'application/json',
-          },
-        },
-      );
-
-      handleHTTPResponseError(assignHolderResponse);
-
-      const { data_exists: dataExistsResponse } =
-        await assignHolderResponse.json();
-      blobAlreadyExists = dataExistsResponse;
-    } catch (e) {
-      throw new Error(
-        `Failed to assign holder: ${
-          getMessageForException(e) ?? 'unknown error'
-        }`,
-      );
-    }
-
-    // 2. Upload blob contents if blob doesn't exist
-    if (!blobAlreadyExists) {
-      const formData = new FormData();
-      formData.append('blob_hash', blobHash);
-      formData.append('blob_data', input.file);
-
-      const xhr = new XMLHttpRequest();
-      const uploadEndpoint = blobService.httpEndpoints.UPLOAD_BLOB;
-      xhr.open(
-        uploadEndpoint.method,
-        makeBlobServiceEndpointURL(uploadEndpoint),
-      );
-      if (options?.timeout) {
-        xhr.timeout = options.timeout;
-      }
-      if (options && options.onProgress) {
-        const { onProgress } = options;
-        xhr.upload.onprogress = _throttle(
-          ({ loaded, total }) => onProgress(loaded / total),
-          50,
-        );
-      }
-
-      let failed = false;
-      const responsePromise = new Promise((resolve, reject) => {
-        xhr.onload = () => {
-          if (failed) {
-            return;
-          }
-          resolve();
-        };
-        xhr.onabort = () => {
-          failed = true;
-          reject(new Error('request aborted'));
-        };
-        xhr.onerror = event => {
-          failed = true;
-          reject(event);
-        };
-        if (options && options.timeout) {
-          xhr.ontimeout = event => {
-            failed = true;
-            reject(event);
-          };
-        }
-        if (options && options.abortHandler) {
-          options.abortHandler(() => {
-            failed = true;
-            reject(new Error('request aborted'));
-            xhr.abort();
-          });
-        }
-      });
-
-      if (!failed) {
-        xhr.send(formData);
-      }
-      await responsePromise;
-    }
-
-    // 3. Send upload metadata to the keyserver, return response
-    const result = await this.props.uploadMediaMetadata({
-      ...input.dimensions,
-      loop: input.loop ?? false,
-      blobHolder,
-      blobHash,
-      encryptionKey: input.encryptionKey,
-      mimeType: input.file.type,
-      filename: input.file.name,
-      thumbHash: input.thumbHash,
-    });
-    return { ...result, blobHolder };
-  }
-
   handleAbortCallback(
     threadID: string,
     localUploadID: string,
@@ -1736,6 +1614,7 @@
     );
     const calendarQuery = useSelector(nonThreadCalendarQuery);
     const callUploadMultimedia = useServerCall(uploadMultimedia);
+    const callBlobServiceUpload = useServerCall(blobServiceUpload);
     const callUploadMediaMetadata = useServerCall(uploadMediaMetadata);
     const callDeleteUpload = useServerCall(deleteUpload);
     const callSendMultimediaMessage = useServerCall(
@@ -1776,6 +1655,7 @@
         calendarQuery={calendarQuery}
         uploadMultimedia={callUploadMultimedia}
         uploadMediaMetadata={callUploadMediaMetadata}
+        blobServiceUpload={callBlobServiceUpload}
         deleteUpload={callDeleteUpload}
         sendMultimediaMessage={callSendMultimediaMessage}
         sendTextMessage={callSendTextMessage}