Repository: wireapp/wire-ios
Pull request #3016: feat: connect UI to wire cells upload use case - WPB-17611
Author: login: samwyndham
Created: 2025-05-14T13:41:58Z | Merged: 2025-05-20T17:19:03Z

<!--do not remove this marker, its needed to replace info when ticket title is updated -->
<!--jira-description-action-hidden-marker-start-->

<table>
<td>
  <a href="https://wearezeta.atlassian.net/browse/WPB-17611" title="WPB-17611" target="_blank"><img alt="Task" src="https://wearezeta.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10818?size=medium" />WPB-17611</a>  [iOS] Integrate stubbed drafts repository that allows adding files for upload
  </td></table>
  <br />
 

<!--jira-description-action-hidden-marker-end-->
<!--do not remove the jira markers to link tickets automatically -->


### Issue

This PR:

- Introduces `WireCellsUploadFileUseCase` which is for now unimplemented.
- Updates the app to upload using the use case when the `WireCells` feature flag is enabled. This includes files attached via the camera button, video button, audio button or paperclip button only.

It also:

- Makes `ConversationInputBarViewController.uploadFile(at:)` private to simplify the public API. Callers can use `ConversationInputBarViewController.uploadFiles(at:)` instead - the version that accepts multiple URLs.
- Converts some `NSURL` instances to `URL` instances. 

### Testing

Please test that this PR doesn't break current behavior:

1. Ensure `WireCells` feature flag is **disabled**.
2. Ensure that all types of files / image / video sending are still working. 

---

### Checklist

- [x] Title contains a reference JIRA issue number like `[WPB-XXX]`.
- [x] Description is filled and free of optional paragraphs.
- [x] Adds/updates automated tests.



Diff:
diff --git a/WireCells/Package.swift b/WireCells/Package.swift
index 42e95f729f4..6337fa0ca27 100644
--- a/WireCells/Package.swift
+++ b/WireCells/Package.swift
@@ -29,7 +29,8 @@ let package = Package(
         .package(url: "https://github.com/awslabs/aws-sdk-swift.git", from: "1.0.0"),
         .package(name: "WireFoundation", path: "../WireFoundation"),
         .package(name: "WireUI", path: "../WireUI"),
-        .package(path: "../WirePlugins")
+        .package(path: "../WirePlugins"),
+        .package(path: "../WireLogging")
     ],
     targets: [
         // Targets are the basic building blocks of a package, defining a module or a test suite.
@@ -40,11 +41,18 @@ let package = Package(
                 .product(name: "CellsSDK", package: "cells-sdk-swift")
             ]
         ),
-        .target(name: "WireCellsBindings"),
+        .target(
+            name: "WireCellsBindings",
+            dependencies: [
+                "WireCellsAPI",
+                "WireCellsImplementation"
+            ]
+        ),
         .target(
             name: "WireCellsImplementation",
             dependencies: [
                 "WireCellsAPI",
+                "WireLogging",
                 .product(name: "AWSS3", package: "aws-sdk-swift"),
                 .product(name: "CellsSDK", package: "cells-sdk-swift")
             ]
diff --git a/WireCells/Sources/WireCellsAPI/UseCases/WireCellsUploadFileUseCaseProtocol.swift b/WireCells/Sources/WireCellsAPI/UseCases/WireCellsUploadFileUseCaseProtocol.swift
new file mode 100644
index 00000000000..dd09001ebd3
--- /dev/null
+++ b/WireCells/Sources/WireCellsAPI/UseCases/WireCellsUploadFileUseCaseProtocol.swift
@@ -0,0 +1,33 @@
+//
+// Wire
+// Copyright (C) 2025 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+public import Foundation
+
+// sourcery: AutoMockable
+/// Uploads file data to the cells server.
+
+public protocol WireCellsUploadFileUseCaseProtocol: Sendable {
+
+    /// Uploads the file at `fileURL` to the cells server.
+
+    func invoke(fileURL: URL) async throws
+
+    /// Creates a file using `imageData` and uploads it to the cells server.
+
+    func invoke(imageData: Data) async throws
+}
diff --git a/WireCells/Sources/WireCellsBindings/WireCellsAssembly.swift b/WireCells/Sources/WireCellsBindings/WireCellsAssembly.swift
index 8c136010478..0b37236404c 100644
--- a/WireCells/Sources/WireCellsBindings/WireCellsAssembly.swift
+++ b/WireCells/Sources/WireCellsBindings/WireCellsAssembly.swift
@@ -16,8 +16,15 @@
 // along with this program. If not, see http://www.gnu.org/licenses/.
 //
 
-public class WireCellsAssembly {
-    public init() {
-        print("Init")
+public import WireCellsAPI
+import WireCellsImplementation
+
+public struct WireCellsAssembly {
+
+    public init() {}
+
+    public func makeUploadFileUseCase() -> any WireCellsUploadFileUseCaseProtocol {
+        WireCellsUploadFileUseCase()
     }
+
 }
diff --git a/WireCells/Sources/WireCellsImplementation/UseCases/WireCellsUploadFileUseCase.swift b/WireCells/Sources/WireCellsImplementation/UseCases/WireCellsUploadFileUseCase.swift
new file mode 100644
index 00000000000..33899f198bd
--- /dev/null
+++ b/WireCells/Sources/WireCellsImplementation/UseCases/WireCellsUploadFileUseCase.swift
@@ -0,0 +1,37 @@
+//
+// Wire
+// Copyright (C) 2025 Wire Swiss GmbH
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see http://www.gnu.org/licenses/.
+//
+
+import Foundation
+import WireCellsAPI
+import WireLogging
+
+package final class WireCellsUploadFileUseCase: WireCellsUploadFileUseCaseProtocol {
+
+    package init() {}
+
+    func invoke(fileURL: URL) async throws {
+        // TODO: [WPB-17619] Implement
+        WireLogger.wireCells.info("Uploading file from URL")
+    }
+
+    func invoke(imageData: Data) async throws {
+        // TODO: [WPB-17619] Implement
+        WireLogger.wireCells.info("Uploading file from image data")
+    }
+
+}
diff --git a/WireLogging/Sources/WireLogging/WireLogger+Instances.swift b/WireLogging/Sources/WireLogging/WireLogger+Instances.swift
index 24d09724b4e..e20dfe4f05d 100644
--- a/WireLogging/Sources/WireLogging/WireLogger+Instances.swift
+++ b/WireLogging/Sources/WireLogging/WireLogger+Instances.swift
@@ -63,4 +63,5 @@ public extension WireLogger {
     static let analytics = WireLogger(tag: "analytics")
     static let supportedProtocols = WireLogger(tag: "supported-protocols")
     static let search = WireLogger(tag: "search")
+    static let wireCells = WireLogger(tag: "wire-cells")
 }
diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj
index 6313431ac20..671d8e72e28 100644
--- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj
+++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj
@@ -109,6 +109,7 @@
 		CB43D9BD2CDBBBB900BF5AEB /* WireFolderPickerUI in Frameworks */ = {isa = PBXBuildFile; productRef = CB43D9BC2CDBBBB900BF5AEB /* WireFolderPickerUI */; };
 		CB4870F22C7F4FE5001E9151 /* WireTransportSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB4870F12C7F4FE5001E9151 /* WireTransportSupport.framework */; };
 		CB4E15122C81CC81005DDEC8 /* Down in Frameworks */ = {isa = PBXBuildFile; productRef = CB4E15112C81CC81005DDEC8 /* Down */; };
+		CBDDB1832DD4BCA700A0A1F3 /* WireCellsBindings in Frameworks */ = {isa = PBXBuildFile; productRef = CBDDB1822DD4BCA700A0A1F3 /* WireCellsBindings */; };
 		E40050762D71A80100B7962B /* WireConversationUI in Frameworks */ = {isa = PBXBuildFile; productRef = E40050752D71A80100B7962B /* WireConversationUI */; };
 		E6C25FC52AFFAAC300406A1C /* WireTesting.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6C25FC42AFFAAC300406A1C /* WireTesting.framework */; };
 		E9816C902CC9244700D77F22 /* WireSyncEngine.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E9816C8F2CC9244700D77F22 /* WireSyncEngine.framework */; };
@@ -939,6 +940,7 @@
 				CB4E15122C81CC81005DDEC8 /* Down in Frameworks */,
 				EEE25B3829719C170008B894 /* WireDataModel.framework in Frameworks */,
 				59D038272C85D31E009FE583 /* WireMainNavigationUI in Frameworks */,
+				CBDDB1832DD4BCA700A0A1F3 /* WireCellsBindings in Frameworks */,
 				767F41E82D7F95C100E4E090 /* WireConversationsUIBindings in Frameworks */,
 				CB1428452DCB8A820092E7AE /* WireCellsUI in Frameworks */,
 				EE33C48D296485FA00C058D1 /* WireShareEngine.framework in Frameworks */,
@@ -1368,6 +1370,7 @@
 				16EE08AD2D92E861001B372C /* WireCoreCryptoUniffi */,
 				CB1428442DCB8A820092E7AE /* WireCellsUI */,
 				59703B272DD4A42000B45773 /* ZIPFoundation */,
+				CBDDB1822DD4BCA700A0A1F3 /* WireCellsBindings */,
 			);
 			productName = "ZClient iOS";
 			productReference = 8F42C538199244A700288E4D /* Wire.app */;
@@ -2963,6 +2966,10 @@
 			package = CB4E15102C81CC81005DDEC8 /* XCRemoteSwiftPackageReference "Down" */;
 			productName = Down;
 		};
+		CBDDB1822DD4BCA700A0A1F3 /* WireCellsBindings */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = WireCellsBindings;
+		};
 		E40050752D71A80100B7962B /* WireConversationUI */ = {
 			isa = XCSwiftPackageProductDependency;
 			productName = WireConversationUI;
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordViewController.swift
index 03ec5f467a8..65f85aeb98c 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordViewController.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordViewController.swift
@@ -471,7 +471,7 @@ final class AudioRecordViewController: UIViewController, AudioRecordBaseViewCont
                 if success {
                     self.delegate?.audioRecordViewControllerWantsToSendAudio(
                         self,
-                        recordingURL: NSURL(fileURLWithPath: convertedPath) as URL,
+                        recordingURL: URL(fileURLWithPath: convertedPath),
                         duration: self.recorder.currentDuration,
                         filter: .none
                     )
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecorder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecorder.swift
index 7a36f6c59e5..a12bd2ce8d0 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecorder.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecorder.swift
@@ -144,12 +144,12 @@ final class AudioRecorder: NSObject, AudioRecorderType {
         }
 
         let fileName = String.filename(for: selfUser).appendingPathExtension(format.fileExtension())!
-        let fileURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
+        let fileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName)
         self.fileURL = fileURL
 
         let audioRecorder = makeAudioRecorder(
             audioFormatID: format.audioFormat(),
-            fileURL: fileURL!
+            fileURL: fileURL
         )
 
         NotificationCenter.default.addObserver(
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Audio.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Audio.swift
index dfbd166276c..c7a6a4de6fa 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Audio.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Audio.swift
@@ -238,7 +238,7 @@ extension ConversationInputBarViewController: AudioRecordViewControllerDelegate
     ) {
 
         let checker = PrivacyWarningChecker(conversation: conversation) { [weak self] in
-            self?.uploadFile(at: recordingURL as URL)
+            self?.uploadFiles(at: [recordingURL])
 
             self?.hideAudioRecordViewController()
         }
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift
index c1266eda88a..5a5a31a2b88 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Camera.swift
@@ -83,7 +83,7 @@ extension ConversationInputBarViewController: CameraKeyboardViewControllerDelega
                 asset: .video(url: videoURL),
                 onConfirm: { [unowned self] _ in
                     dismiss(animated: true)
-                    uploadFile(at: videoURL)
+                    uploadFiles(at: [videoURL])
                 },
                 onCancel: { [unowned self] in
                     dismiss(animated: true) {
@@ -164,17 +164,29 @@ extension ConversationInputBarViewController: CameraKeyboardViewControllerDelega
 
         let context = ConfirmAssetViewController.Context(
             asset: .image(mediaAsset: mediaAsset),
-            onConfirm: { [weak self] (editedImage: UIImage?) in
+            onConfirm: { [weak self, wireCellsUploadFileUseCase] (editedImage: UIImage?) in
                 guard let self else { return }
                 dismiss(animated: true) {
                     self.writeToSavedPhotoAlbumIfNecessary(
                         imageData: imageData,
                         isFromCamera: isFromCamera
                     )
-                    self.sendController.sendMessage(
-                        withImageData: editedImage?.pngData() ?? imageData,
-                        userSession: self.userSession
-                    )
+                    let dataToSend = editedImage?.pngData() ?? imageData
+                    if DeveloperFlag.wireCells.isOn {
+                        Task.detached {
+                            // We don't care about the result of the operation here as we will be observing changes.
+                            do {
+                                try await wireCellsUploadFileUseCase.invoke(imageData: dataToSend)
+                            } catch {
+                                WireLogger.conversation.error("Failed to upload file: \(error)")
+                            }
+                        }
+                    } else {
+                        self.sendController.sendMessage(
+                            withImageData: dataToSend,
+                            userSession: self.userSession
+                        )
+                    }
                 }
             },
             onCancel: { [weak self] in
@@ -228,7 +240,7 @@ extension ConversationInputBarViewController: CameraKeyboardViewControllerDelega
         let filename: String = ((lastPathComponent as NSString).deletingPathExtension as NSString)
             .appendingPathExtension("mp4") ?? "video.mp4"
 
-        let videoURLAsset = AVURLAsset(url: NSURL(fileURLWithPath: inputPath) as URL)
+        let videoURLAsset = AVURLAsset(url: URL(fileURLWithPath: inputPath))
 
         videoURLAsset
             .convert(
@@ -263,7 +275,7 @@ extension ConversationInputBarViewController: UIVideoEditorControllerDelegate {
                 return
             }
 
-            self.uploadFile(at: NSURL(fileURLWithPath: path) as URL)
+            self.uploadFiles(at: [URL(fileURLWithPath: path)])
         }
     }
 
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Files.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Files.swift
index ca67a408de3..ac6a549850f 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Files.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+Files.swift
@@ -17,7 +17,9 @@
 //
 
 import Foundation
+import WireCellsBindings
 import WireCommonComponents
+import WireLogging
 import WireSyncEngine
 
 extension ConversationInputBarViewController: UINavigationControllerDelegate {}
@@ -39,30 +41,41 @@ extension ConversationInputBarViewController {
         return true
     }
 
+    /// Upload files at the given `urls`.
+    ///
+    /// - parameter urls: The URLs of the files to upload.
+    /// - note: If wire cells is enabled, each file will be uploaded separately. If wire cells is disabled and there are
+    /// multiple files, these will be zipped and then uploaded.
+
     func uploadFiles(at urls: [URL]) {
-        guard urls.count > 1 else {
-            if let url = urls.first {
-                uploadFile(at: url)
+        guard !urls.isEmpty else { return }
+
+        if DeveloperFlag.wireCells.isOn {
+            Task.detached { [wireCellsUploadFileUseCase] in
+                // We don't care about the result of the operation here as we will be observing changes.
+                for url in urls {
+                    do {
+                        try await wireCellsUploadFileUseCase.invoke(fileURL: url)
+                    } catch {
+                        WireLogger.conversation.error("Failed to upload file: \(error)")
+                    }
+                }
             }
-            return
-        }
-
-        if let archiveURL = urls.zipFiles() {
+        } else if urls.count == 1 {
+            uploadFile(at: urls[0])
+        } else if let archiveURL = urls.zipFiles() {
             uploadFile(at: archiveURL)
         } else {
             zmLog.error("Cannot archive files at URLs: \(urls.description)")
         }
-
     }
 
-    /// upload a signal file
+    /// upload a single file
     ///
     /// - Parameter url: the URL of the file
-    func uploadFile(at url: URL) {
+    private func uploadFile(at url: URL) {
         guard let conversation = conversation as? ZMConversation else { return }
 
-        guard let maxUploadFileSize = ZMUserSession.shared()?.maxUploadFileSize else { return }
-
         let completion: Completion = { [weak self] in
             self?.removeItem(atPath: url.path)
         }
@@ -73,7 +86,7 @@ extension ConversationInputBarViewController {
             return completion()
         }
 
-        guard fileSize <= maxUploadFileSize else {
+        guard fileSize <= userSession.maxUploadFileSize else {
             // file exceeds maximum allowed upload size
             parent?.dismiss(animated: false)
             showAlertForFileTooBig()
@@ -118,9 +131,10 @@ extension ConversationInputBarViewController {
     }
 
     private func showAlertForFileTooBig() {
-        guard let maxUploadFileSize = ZMUserSession.shared()?.maxUploadFileSize else { return }
-
-        let maxSizeString = ByteCountFormatter.string(fromByteCount: Int64(maxUploadFileSize), countStyle: .binary)
+        let maxSizeString = ByteCountFormatter.string(
+            fromByteCount: Int64(userSession.maxUploadFileSize),
+            countStyle: .binary
+        )
         let errorMessage = L10n.Localizable.Content.File.tooBig(maxSizeString)
 
         let alert = UIAlertController(
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+FilesPopover.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+FilesPopover.swift
index 584aff2f448..4502f063da7 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+FilesPopover.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+FilesPopover.swift
@@ -46,7 +46,7 @@ extension ConversationInputBarViewController {
                         .appendingPathComponent(sourceLocation.lastPathComponent)
 
                     try? FileManager.default.copyItem(at: sourceLocation, to: destLocation)
-                    self.uploadFile(at: destLocation)
+                    self.uploadFiles(at: [destLocation])
                 }
             }
 
@@ -190,7 +190,7 @@ extension ConversationInputBarViewController {
                         let fileURL = dir.appendingPathComponent(fileName)
                         try? randomData.write(to: fileURL)
 
-                        self.uploadFile(at: fileURL)
+                        self.uploadFiles(at: [fileURL])
                     }
                 }
             })
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift
index bcf60a6e46d..6a2a4d154eb 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController+ImagePicker.swift
@@ -35,7 +35,7 @@ extension ConversationInputBarViewController {
             if UIDevice.isSimulator {
                 let testFilePath = "/var/tmp/video.mp4"
                 if FileManager.default.fileExists(atPath: testFilePath) {
-                    uploadFile(at: URL(fileURLWithPath: testFilePath))
+                    uploadFiles(at: [URL(fileURLWithPath: testFilePath)])
                 }
             }
             return
@@ -120,7 +120,7 @@ extension ConversationInputBarViewController {
             ) { resultURL, _, error in
                 if error == nil,
                    let resultURL {
-                    self.uploadFile(at: resultURL)
+                    self.uploadFiles(at: [resultURL])
                 }
 
                 self.parent?.dismiss(animated: true)
diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController/ConversationInputBarViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController/ConversationInputBarViewController.swift
index cf1cff47236..d3238ec92c7 100644
--- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController/ConversationInputBarViewController.swift
+++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/ConversationInputBarViewController/ConversationInputBarViewController.swift
@@ -22,6 +22,8 @@ import MobileCoreServices
 import Photos
 import SwiftUI
 import UIKit
+import WireCellsAPI
+import WireCellsBindings
 import WireCellsUI
 import WireCommonComponents
 import WireDesign
@@ -223,6 +225,7 @@ final class ConversationInputBarViewController: UIViewController,
     private var typingObserverToken: Any?
     let userSession: UserSession
     let fileMetaDataGenerator: FileMetaDataGeneratorProtocol
+    let wireCellsUploadFileUseCase: WireCellsUploadFileUseCaseProtocol
 
     private var inputBarButtons: [IconButton] {
         var buttonsArray: [IconButton] = []
@@ -347,13 +350,15 @@ final class ConversationInputBarViewController: UIViewController,
         conversation: InputBarConversationType,
         userSession: UserSession,
         classificationProvider: (any SecurityClassificationProviding)?,
-        networkStatusObservable: any NetworkStatusObservable
+        networkStatusObservable: any NetworkStatusObservable,
+        wireCellsAssembly: WireCellsAssembly = WireCellsAssembly()
     ) {
         self.conversation = conversation
         self.userSession = userSession
         self.classificationProvider = classificationProvider
         self.networkStatusObservable = networkStatusObservable
         self.fileMetaDataGenerator = FileMetaDataGenerator.shared
+        self.wireCellsUploadFileUseCase = wireCellsAssembly.makeUploadFileUseCase()
 
         super.init(nibName: nil, bundle: nil)
 


Reviews:
user: login: samwyndham
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:43:17Z
user: login: samwyndham
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T13:44:17Z
user: login: dmitrysimkin
body: Looks good 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T10:09:34Z
user: login: johnxnguyen
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-19T08:10:37Z

Review comments:
diff_hunk: @@ -16,8 +16,15 @@
 // along with this program. If not, see http://www.gnu.org/licenses/.
 //
 
-public class WireCellsAssembly {
-    public init() {
-        print("Init")
+public import WireCellsAPI
+import WireCellsImplementation
+
+public struct WireCellsAssembly {
+
+    public init() {}
+
+    public func makeUploadFileUseCase() -> any WireCellsUploadFileUseCaseProtocol {
path: WireCells/Sources/WireCellsBindings/WireCellsAssembly.swift
user: login: samwyndham
body: This API will likely change as I understand better the requirements for implementing `WireCellsUploadFileUseCase`
created_at: 2025-05-14T13:43:17Z
author_association: CONTRIBUTOR
path: wire-ios/Wire-iOS/Sources/UserInterface/Conversation/InputBar/AudioRecordViewController.swift
user: login: samwyndham
body: Some cleanup unrelated to the PR.
created_at: 2025-05-14T13:44:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,7 @@ public extension WireLogger {
     static let backgroundActivity = WireLogger(tag: "background-activity")
     static let badgeCount = WireLogger(tag: "badge-count")
     static let calling = WireLogger(tag: "calling")
+    static let cells = WireLogger(tag: "cells")
path: WireLogging/Sources/WireLogging/WireLogger+Instances.swift
user: login: johnxnguyen
body: question: I wonder if `cells` is understood as "Wire Cells" by most people. 
created_at: 2025-05-19T08:08:54Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-14T14:00:55Z
body: ## Test Results
    4 files    293 suites   3m 57s ⏱️
1 826 tests 1 797 ✅ 26 💤 3 ❌
1 826 runs  1 800 ✅ 26 💤 0 ❌

For more details on these failures, see [this check](https://github.com/wireapp/wire-ios/runs/42561546954).

Results for commit 6c8c2738.

[test-results]:data:application/gzip;base64,H4sIANOPLGgC/02MzQ6DIBAGX8Vw7gFBWejLNIQuCalKw8/J9N27mBY9zuy3szMfFszsPky3geUaygHCSMJnTbaEuDUhgQQdSzuPWqg/PnJ1rjkwcLpXeLevc+VtWMjILjClmMhwMqlul2yjXtWcd3WJHuLX7INr0sV1DYWAKaedAKk9cm4doOUWxDx6g+MsQBmcQKNQnn2+/usJkQ0BAAA=

:recycle: This comment has been updated with latest results.
author_association: CONTRIBUTOR
user: login: datadog-wireapp[bot]
created_at: 2025-05-14T14:04:06Z
body: 
## Datadog Report

Branch report: [`feat/cells-add-attachment-UI-to-use-case-WPB-17611`](https://app.datadoghq.eu/ci/test-branch/github.com%2Fwireapp%2Fwire-ios/wire-ios-mono/feat%2Fcells-add-attachment-UI-to-use-case-WPB-17611?env=ci&index=citest&_user_click_origin_=github)
Commit report: [`6866641`](https://app.datadoghq.eu/ci/test-commit/github.com%2Fwireapp%2Fwire-ios/wire-ios-mono/feat%2Fcells-add-attachment-UI-to-use-case-WPB-17611/68666413653ea94e8ced14763f95609ce3a3949e?env=ci&index=citest&_user_click_origin_=github)
Test service: `wire-ios-mono`

:white_check_mark: 0 Failed, 1800 Passed, 26 Skipped, 2m 5.69s Total Time
:snowflake: 2 New Flaky



### New Flaky Tests (2)
* `test_Login_withEmail()` - `WireUITests.WireAuthenticationTests` - [Last Failure](https://app.datadoghq.eu/ci/test/AwAAAZbuEwnNRX69LgAAABhBWmJ1RXduTkFBQ1pYQlZOUEIyNFloY3oAAAAkMDE5NmVlMTQtZGQ1Mi00Y2E0LWI2ZWItYzg4NGFlZmFlNmQzAAABNg?colorBy=service&spanViewType=metadata&env=ci&spanID=3681626065383142223&eventStack=AwAAAZbuEwnNRX69LgAAABhBWmJ1RXduTkFBQ1pYQlZOUEIyNFloY3oAAAAkMDE5NmVlMTQtZGQ1Mi00Y2E0LWI2ZWItYzg4NGFlZmFlNmQzAAABNg)

	<details>
	<summary>Expand for error</summary>

	```
	Test has failed
	```
	</details>
* `test_Login_withWrongEmail_NextIsDisabled()` - `WireUITests.WireAuthenticationTests` - [Last Failure](https://app.datadoghq.eu/ci/test/AwAAAZbuEwnNRX69KQAAABhBWmJ1RXduTkFBQmJ6anpuZVQwUzh3Z2sAAAAkMDE5NmVlMTQtZGQ1Mi00Y2E0LWI2ZWItYzg4NGFlZmFlNmQzAAABMQ?colorBy=service&spanViewType=metadata&env=ci&spanID=2596534491537429784&eventStack=AwAAAZbuEwnNRX69KQAAABhBWmJ1RXduTkFBQmJ6anpuZVQwUzh3Z2sAAAAkMDE5NmVlMTQtZGQ1Mi00Y2E0LWI2ZWItYzg4NGFlZmFlNmQzAAABMQ)

	<details>
	<summary>Expand for error</summary>

	```
	Test has failed
	```
	</details>


author_association: NONE