diff --git a/Examples/xx-messenger/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift b/Examples/xx-messenger/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift
new file mode 100644
index 0000000000000000000000000000000000000000..84abe3c5edf058f205978bc836758a52bd379e0f
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/ReceiveFileHandler/ReceiveFileHandler.swift
@@ -0,0 +1,128 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+
+public struct ReceiveFileHandler {
+  public struct ProgressError: Error {
+    public init(message: String) {
+      self.message = message
+    }
+
+    public var message: String
+  }
+
+  public typealias OnError = (Error) -> Void
+
+  public var run: (@escaping OnError) -> Cancellable
+
+  public func callAsFunction(onError: @escaping OnError) -> Cancellable {
+    run(onError)
+  }
+}
+
+extension ReceiveFileHandler {
+  public static func live(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    now: @escaping () -> Date
+  ) -> ReceiveFileHandler {
+    ReceiveFileHandler { onError in
+      func receiveFile(_ file: ReceivedFile) {
+        do {
+          let date = now()
+          try db().saveFileTransfer(XXModels.FileTransfer(
+            id: file.transferId,
+            contactId: file.senderId,
+            name: file.name,
+            type: file.type,
+            data: nil,
+            progress: 0,
+            isIncoming: true,
+            createdAt: date
+          ))
+          try db().saveMessage(XXModels.Message(
+            senderId: file.senderId,
+            recipientId: try messenger.e2e.tryGet().getContact().getId(),
+            groupId: nil,
+            date: date,
+            status: .received,
+            isUnread: false,
+            text: "",
+            fileTransferId: file.transferId
+          ))
+          try messenger.receiveFile(.init(
+            transferId: file.transferId,
+            callbackIntervalMS: 500
+          )) { info in
+            switch info {
+            case .progress(let transmitted, let total):
+              updateProgress(
+                transferId: file.transferId,
+                transmitted: transmitted,
+                total: total
+              )
+
+            case .finished(let data):
+              saveData(
+                transferId: file.transferId,
+                data: data
+              )
+
+            case .failed(.receiveError(let error)):
+              onError(error)
+
+            case .failed(.callbackError(let error)):
+              onError(error)
+
+            case .failed(.progressError(let message)):
+              onError(ProgressError(message: message))
+            }
+          }
+        } catch {
+          onError(error)
+        }
+      }
+
+      func updateProgress(transferId: Data, transmitted: Int, total: Int) {
+        do {
+          if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first {
+            transfer.progress = total > 0 ? Float(transmitted) / Float(total) : 0
+            try db().saveFileTransfer(transfer)
+          }
+        } catch {
+          onError(error)
+        }
+      }
+
+      func saveData(transferId: Data, data: Data) {
+        do {
+          if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first {
+            transfer.progress = 1
+            transfer.data = data
+            try db().saveFileTransfer(transfer)
+          }
+        } catch {
+          onError(error)
+        }
+      }
+
+      return messenger.registerReceiveFileCallback(.init { result in
+        switch result {
+        case .success(let file):
+          receiveFile(file)
+
+        case .failure(let error):
+          onError(error)
+        }
+      })
+    }
+  }
+}
+
+extension ReceiveFileHandler {
+  public static let unimplemented = ReceiveFileHandler(
+    run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {})
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/SendImage/SendImage.swift b/Examples/xx-messenger/Sources/AppCore/SendImage/SendImage.swift
new file mode 100644
index 0000000000000000000000000000000000000000..a457dcd49e11c3e06975a7151a6bd8a9dd839412
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/SendImage/SendImage.swift
@@ -0,0 +1,119 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+
+public struct SendImage {
+  public struct ProgressError: Error, Equatable {
+    public init(message: String) {
+      self.message = message
+    }
+
+    public var message: String
+  }
+
+  public typealias OnError = (Error) -> Void
+  public typealias Completion = () -> Void
+
+  public var run: (Data, Data, @escaping OnError, @escaping Completion) -> Void
+
+  public func callAsFunction(
+    _ image: Data,
+    to recipientId: Data,
+    onError: @escaping OnError,
+    completion: @escaping Completion
+  ) {
+    run(image, recipientId, onError, completion)
+  }
+}
+
+extension SendImage {
+  public static func live(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    now: @escaping () -> Date
+  ) -> SendImage {
+    SendImage { image, recipientId, onError, completion in
+      func updateProgress(transferId: Data, progress: Float) {
+        do {
+          if var transfer = try db().fetchFileTransfers(.init(id: [transferId])).first {
+            transfer.progress = progress
+            try db().saveFileTransfer(transfer)
+          }
+        } catch {
+          onError(error)
+        }
+      }
+
+      let file = FileSend(
+        name: "image.jpg",
+        type: "image",
+        preview: nil,
+        contents: image
+      )
+      let params = MessengerSendFile.Params(
+        file: file,
+        recipientId: recipientId,
+        retry: 2,
+        callbackIntervalMS: 500
+      )
+      do {
+        let date = now()
+        let myContactId = try messenger.e2e.tryGet().getContact().getId()
+        let transferId = try messenger.sendFile(params) { info in
+          switch info {
+          case .progress(let transferId, let transmitted, let total):
+            updateProgress(
+              transferId: transferId,
+              progress: total > 0 ? Float(transmitted) / Float(total) : 0
+            )
+
+          case .finished(let transferId):
+            updateProgress(
+              transferId: transferId,
+              progress: 1
+            )
+
+          case .failed(_, .error(let error)):
+            onError(error)
+
+          case .failed(_, .progressError(let message)):
+            onError(ProgressError(message: message))
+
+          case .failed(_, .close(let error)):
+            onError(error)
+          }
+        }
+        try db().saveFileTransfer(XXModels.FileTransfer(
+          id: transferId,
+          contactId: myContactId,
+          name: file.name,
+          type: file.type,
+          data: image,
+          progress: 0,
+          isIncoming: false,
+          createdAt: date
+        ))
+        try db().saveMessage(XXModels.Message(
+          senderId: myContactId,
+          recipientId: recipientId,
+          groupId: nil,
+          date: date,
+          status: .sent,
+          isUnread: false,
+          text: "",
+          fileTransferId: transferId
+        ))
+      } catch {
+        onError(error)
+      }
+    }
+  }
+}
+
+extension SendImage {
+  public static let unimplemented = SendImage(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/ImagePicker.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/ImagePicker.swift
new file mode 100644
index 0000000000000000000000000000000000000000..c0b5fbadd05543205ae7c87dfbdc5daa5d1a1f2b
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/ImagePicker.swift
@@ -0,0 +1,49 @@
+import UIKit
+import SwiftUI
+
+public struct ImagePicker: UIViewControllerRepresentable {
+  public init(onImport: @escaping (UIImage) -> Void) {
+    self.onImport = onImport
+  }
+
+  var onImport: (UIImage) -> Void
+  @Environment(\.presentationMode) private var presentationMode
+
+  public func makeCoordinator() -> Coordinator {
+    Coordinator(self)
+  }
+
+  public func makeUIViewController(context: Context) -> UIImagePickerController {
+    let controller = UIImagePickerController()
+    controller.delegate = context.coordinator
+    return controller
+  }
+
+  public func updateUIViewController(
+    _ uiViewController: UIImagePickerController,
+    context: Context
+  ) {}
+}
+
+extension ImagePicker {
+  public final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
+    init(_ picker: ImagePicker) {
+      self.picker = picker
+      super.init()
+    }
+
+    public func imagePickerController(
+      _ controller: UIImagePickerController,
+      didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]
+    ) {
+      if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
+        DispatchQueue.main.async {
+          self.picker.onImport(image)
+        }
+      }
+      picker.presentationMode.wrappedValue.dismiss()
+    }
+
+    let picker: ImagePicker
+  }
+}
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index c4ff9b27f027a0eef0d02eaeab6e8e7db519025c..2b1f9964c4b900bee5596eb7fee1e1f59ef4f7c8 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
@@ -99,6 +99,11 @@ extension AppEnvironment {
             db: dbManager.getDB,
             now: Date.init
           ),
+          sendImage: .live(
+            messenger: messenger,
+            db: dbManager.getDB,
+            now: Date.init
+          ),
           mainQueue: mainQueue,
           bgQueue: bgQueue
         )
@@ -113,6 +118,11 @@ extension AppEnvironment {
         messenger: messenger,
         db: dbManager.getDB
       ),
+      receiveFileHandler: .live(
+        messenger: messenger,
+        db: dbManager.getDB,
+        now: Date.init
+      ),
       backupStorage: backupStorage,
       log: .live(),
       mainQueue: mainQueue,
diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
index 7796f592bcd5c795c20d4a281d243cd03a7b8df9..56f692c6c1b1cb83f010ec479fb2d82058f4b25e 100644
--- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
+++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift
@@ -50,6 +50,7 @@ struct AppEnvironment {
   var messenger: Messenger
   var authHandler: AuthCallbackHandler
   var messageListener: MessageListenerHandler
+  var receiveFileHandler: ReceiveFileHandler
   var backupStorage: BackupStorage
   var log: Logger
   var mainQueue: AnySchedulerOf<DispatchQueue>
@@ -66,6 +67,7 @@ extension AppEnvironment {
     messenger: .unimplemented,
     authHandler: .unimplemented,
     messageListener: .unimplemented,
+    receiveFileHandler: .unimplemented,
     backupStorage: .unimplemented,
     log: .unimplemented,
     mainQueue: .unimplemented,
@@ -98,6 +100,10 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment>
         cancellables.append(env.messageListener(onError: { error in
           env.log(.error(error as NSError))
         }))
+        cancellables.append(env.receiveFileHandler(onError: { error in
+          env.log(.error(error as NSError))
+        }))
+
         cancellables.append(env.messenger.registerBackupCallback(.init { data in
           try? env.backupStorage.store(data)
         }))
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift
index 46cfdbfa7103029615bd525abd78d8f54dfa3a10..3538bc2536f23e493a298f2761fd3614711f77da 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift
@@ -18,13 +18,15 @@ public struct ChatState: Equatable, Identifiable {
       date: Date,
       senderId: Data,
       text: String,
-      status: XXModels.Message.Status
+      status: XXModels.Message.Status,
+      fileTransfer: XXModels.FileTransfer? = nil
     ) {
       self.id = id
       self.date = date
       self.senderId = senderId
       self.text = text
       self.status = status
+      self.fileTransfer = fileTransfer
     }
 
     public var id: Int64
@@ -32,6 +34,7 @@ public struct ChatState: Equatable, Identifiable {
     public var senderId: Data
     public var text: String
     public var status: XXModels.Message.Status
+    public var fileTransfer: XXModels.FileTransfer?
   }
 
   public init(
@@ -63,6 +66,7 @@ public enum ChatAction: Equatable, BindableAction {
   case didFetchMessages(IdentifiedArrayOf<ChatState.Message>)
   case sendTapped
   case sendFailed(String)
+  case imagePicked(Data)
   case dismissSendFailureTapped
   case binding(BindingAction<ChatState>)
 }
@@ -72,12 +76,14 @@ public struct ChatEnvironment {
     messenger: Messenger,
     db: DBManagerGetDB,
     sendMessage: SendMessage,
+    sendImage: SendImage,
     mainQueue: AnySchedulerOf<DispatchQueue>,
     bgQueue: AnySchedulerOf<DispatchQueue>
   ) {
     self.messenger = messenger
     self.db = db
     self.sendMessage = sendMessage
+    self.sendImage = sendImage
     self.mainQueue = mainQueue
     self.bgQueue = bgQueue
   }
@@ -85,6 +91,7 @@ public struct ChatEnvironment {
   public var messenger: Messenger
   public var db: DBManagerGetDB
   public var sendMessage: SendMessage
+  public var sendImage: SendImage
   public var mainQueue: AnySchedulerOf<DispatchQueue>
   public var bgQueue: AnySchedulerOf<DispatchQueue>
 }
@@ -95,6 +102,7 @@ extension ChatEnvironment {
     messenger: .unimplemented,
     db: .unimplemented,
     sendMessage: .unimplemented,
+    sendImage: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented
   )
@@ -112,31 +120,50 @@ public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment>
       let myContactId = try env.messenger.e2e.tryGet().getContact().getId()
       state.myContactId = myContactId
       let queryChat: XXModels.Message.Query.Chat
+      let receivedFileTransfersQuery: XXModels.FileTransfer.Query
+      let sentFileTransfersQuery: XXModels.FileTransfer.Query
       switch state.id {
       case .contact(let contactId):
         queryChat = .direct(myContactId, contactId)
+        receivedFileTransfersQuery = .init(
+          contactId: contactId,
+          isIncoming: true
+        )
+        sentFileTransfersQuery = .init(
+          contactId: myContactId,
+          isIncoming: false
+        )
       }
-      let query = XXModels.Message.Query(chat: queryChat)
-      return try env.db().fetchMessagesPublisher(query)
-        .assertNoFailure()
-        .map { messages in
-          messages.compactMap { message in
-            guard let id = message.id else { return nil }
-            return ChatState.Message(
-              id: id,
-              date: message.date,
-              senderId: message.senderId,
-              text: message.text,
-              status: message.status
-            )
-          }
+      let messagesQuery = XXModels.Message.Query(chat: queryChat)
+      return Publishers.CombineLatest3(
+        try env.db().fetchMessagesPublisher(messagesQuery),
+        try env.db().fetchFileTransfersPublisher(receivedFileTransfersQuery),
+        try env.db().fetchFileTransfersPublisher(sentFileTransfersQuery)
+      )
+      .map { messages, receivedFileTransfers, sentFileTransfers in
+        (messages, receivedFileTransfers + sentFileTransfers)
+      }
+      .assertNoFailure()
+      .map { messages, fileTransfers in
+        messages.compactMap { message in
+          guard let id = message.id else { return nil }
+          return ChatState.Message(
+            id: id,
+            date: message.date,
+            senderId: message.senderId,
+            text: message.text,
+            status: message.status,
+            fileTransfer: fileTransfers.first { $0.id == message.fileTransferId }
+          )
         }
-        .map { IdentifiedArrayOf<ChatState.Message>(uniqueElements: $0) }
-        .map(ChatAction.didFetchMessages)
-        .subscribe(on: env.bgQueue)
-        .receive(on: env.mainQueue)
-        .eraseToEffect()
-        .cancellable(id: FetchEffectId.self, cancelInFlight: true)
+      }
+      .removeDuplicates()
+      .map { IdentifiedArrayOf<ChatState.Message>(uniqueElements: $0) }
+      .map(ChatAction.didFetchMessages)
+      .subscribe(on: env.bgQueue)
+      .receive(on: env.mainQueue)
+      .eraseToEffect()
+      .cancellable(id: FetchEffectId.self, cancelInFlight: true)
     } catch {
       state.failure = error.localizedDescription
       return .none
@@ -174,6 +201,28 @@ public let chatReducer = Reducer<ChatState, ChatAction, ChatEnvironment>
     state.sendFailure = failure
     return .none
 
+  case .imagePicked(let data):
+    let chatId = state.id
+    return Effect.run { subscriber in
+      switch chatId {
+      case .contact(let recipientId):
+        env.sendImage(
+          data,
+          to: recipientId,
+          onError: { error in
+            subscriber.send(.sendFailed(error.localizedDescription))
+          },
+          completion: {
+            subscriber.send(completion: .finished)
+          }
+        )
+      }
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgQueue)
+    .receive(on: env.mainQueue)
+    .eraseToEffect()
+
   case .dismissSendFailureTapped:
     state.sendFailure = nil
     return .none
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
index 23596dc1ceecc06d65cc6342318278f84313f03a..6058b61bc86fe9c54f25483e7de2a05c5a35dc9d 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
@@ -8,6 +8,7 @@ public struct ChatView: View {
   }
 
   let store: Store<ChatState, ChatAction>
+  @State var isPresentingImagePicker = false
 
   struct ViewState: Equatable {
     var myContactId: Data?
@@ -87,12 +88,28 @@ public struct ChatView: View {
             ))
             .textFieldStyle(.roundedBorder)
 
-            Button {
-              viewStore.send(.sendTapped)
-            } label: {
-              Image(systemName: "paperplane.fill")
+            if viewStore.text.isEmpty == false {
+              Button {
+                viewStore.send(.sendTapped)
+              } label: {
+                Image(systemName: "paperplane.fill")
+              }
+              .buttonStyle(.borderedProminent)
+            } else {
+              Button {
+                isPresentingImagePicker = true
+              } label: {
+                Image(systemName: "photo.on.rectangle.angled")
+              }
+              .buttonStyle(.borderedProminent)
+              .sheet(isPresented: $isPresentingImagePicker) {
+                ImagePicker { image in
+                  if let data = image.jpegData(compressionQuality: 0.7) {
+                    viewStore.send(.imagePicked(data))
+                  }
+                }
+              }
             }
-            .buttonStyle(.borderedProminent)
           }
           .padding()
         }
@@ -112,6 +129,10 @@ public struct ChatView: View {
       message.senderId == myContactId ? .trailing : .leading
     }
 
+    var paddingEdge: Edge.Set {
+      message.senderId == myContactId ? .leading : .trailing
+    }
+
     var textColor: Color? {
       message.senderId == myContactId ? Color.white : nil
     }
@@ -123,20 +144,38 @@ public struct ChatView: View {
           .font(.footnote)
           .frame(maxWidth: .infinity, alignment: alignment)
 
-        Text(message.text)
-          .foregroundColor(textColor)
-          .padding(.horizontal, 16)
-          .padding(.vertical, 8)
-          .background {
-            if message.senderId == myContactId {
-              RoundedRectangle(cornerRadius: 16, style: .continuous)
-                .fill(Color.blue)
-            } else {
-              RoundedRectangle(cornerRadius: 16, style: .continuous)
-                .fill(Material.ultraThick)
+        VStack(alignment: .leading) {
+          if let fileTransfer = message.fileTransfer {
+            Text("\(fileTransfer.name) (\(fileTransfer.type))")
+            if fileTransfer.progress < 1 {
+              ProgressView(value: fileTransfer.progress)
+            }
+            if fileTransfer.type == "image",
+               let data = fileTransfer.data,
+               let image = UIImage(data: data) {
+              Image(uiImage: image)
+                .resizable()
+                .scaledToFit()
+                .padding(.bottom, 8)
             }
+          } else {
+            Text(message.text)
           }
-          .frame(maxWidth: .infinity, alignment: alignment)
+        }
+        .foregroundColor(textColor)
+        .padding(.horizontal, 16)
+        .padding(.vertical, 8)
+        .background {
+          if message.senderId == myContactId {
+            RoundedRectangle(cornerRadius: 16, style: .continuous)
+              .fill(Color.blue)
+          } else {
+            RoundedRectangle(cornerRadius: 16, style: .continuous)
+              .fill(Material.ultraThick)
+          }
+        }
+        .frame(maxWidth: .infinity, alignment: alignment)
+        .padding(paddingEdge, 60)
       }
       .padding(.horizontal)
     }
@@ -178,6 +217,47 @@ public struct ChatView_Previews: PreviewProvider {
               text: "Hi!",
               status: .sent
             ),
+            .init(
+              id: 3,
+              date: Date(),
+              senderId: "contact-id".data(using: .utf8)!,
+              text: "",
+              status: .received,
+              fileTransfer: .init(
+                id: Data(),
+                contactId: Data(),
+                name: "received_file.jpeg",
+                type: "image",
+                progress: 0.75,
+                isIncoming: true
+              )
+            ),
+            .init(
+              id: 4,
+              date: Date(),
+              senderId: "my-contact-id".data(using: .utf8)!,
+              text: "",
+              status: .sent,
+              fileTransfer: .init(
+                id: Data(),
+                contactId: Data(),
+                name: "sent_file.jpeg",
+                type: "image",
+                data: {
+                  let bounds = CGRect(origin: .zero, size: .init(width: 4, height: 3))
+                  let format = UIGraphicsImageRendererFormat()
+                  format.scale = 1
+                  let renderer = UIGraphicsImageRenderer(bounds: bounds, format: format)
+                  let image = renderer.image { ctx in
+                    UIColor.systemMint.setFill()
+                    ctx.fill(bounds)
+                  }
+                  return image.jpegData(compressionQuality: 0.72)
+                }(),
+                progress: 1,
+                isIncoming: true
+              )
+            ),
           ],
           failure: "Something went wrong when fetching messages from database.",
           sendFailure: "Something went wrong when sending message."
diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
index f2015ebc2434ecaada1ba9abd4d345cea91bd03e..6433dd330faac74045cad472798cd8c06a79cb40 100644
--- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
+++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift
@@ -151,6 +151,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
             try env.messenger.listenForMessages()
           }
 
+          if env.messenger.isFileTransferRunning() == false {
+            try env.messenger.startFileTransfer()
+          }
+
           if env.messenger.isLoggedIn() == false {
             if try env.messenger.isRegistered() == false {
               return .success(.messenger(.didStartUnregistered))
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AppCoreTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AppCoreTests.swift
deleted file mode 100644
index 9a5658d4caa59cea2181061ff1ca1d2d6d38c5e4..0000000000000000000000000000000000000000
--- a/Examples/xx-messenger/Tests/AppCoreTests/AppCoreTests.swift
+++ /dev/null
@@ -1,9 +0,0 @@
-import XCTest
-@testable import AppCore
-
-@MainActor
-final class AppCoreTests: XCTestCase {
-  func testExample() async throws {
-    XCTAssert(true)
-  }
-}
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/ReceiveFileHandler/ReceiveFileHandlerTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/ReceiveFileHandler/ReceiveFileHandlerTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..6d54a9cfcc27db703a70f723b29bbc0833e1f5b1
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/ReceiveFileHandler/ReceiveFileHandlerTests.swift
@@ -0,0 +1,214 @@
+import CustomDump
+import XCTest
+import XXMessengerClient
+import XXClient
+import XXModels
+@testable import AppCore
+
+final class ReceiveFileHandlerTests: XCTestCase {
+  func testReceiveFile() {
+    let currentDate = Date(timeIntervalSince1970: 123)
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let receivedFile = ReceivedFile.stub()
+
+    var actions: [Action] = []
+    var receiveFileCallback: ReceiveFileCallback?
+    var receivingFileCallback: MessengerReceiveFile.Callback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.registerReceiveFileCallback.run = { callback in
+      actions.append(.didRegisterReceiveFileCallback)
+      receiveFileCallback = callback
+      return Cancellable { actions.append(.didCancelReceiveFileCallback) }
+    }
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in myContactId }
+        return contact
+      }
+      return e2e
+    }
+    messenger.receiveFile.run = { params, callback in
+      actions.append(.didReceiveFile(params))
+      receivingFileCallback = callback
+    }
+
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveFileTransfer.run = { model in
+        actions.append(.didSaveFileTransfer(model))
+        return model
+      }
+      db.saveMessage.run = { model in
+        actions.append(.didSaveMessage(model))
+        return model
+      }
+      db.fetchFileTransfers.run = { query in
+        actions.append(.didFetchFileTransfers(query))
+        return [
+          FileTransfer(
+            id: receivedFile.transferId,
+            contactId: receivedFile.senderId,
+            name: receivedFile.name,
+            type: receivedFile.type,
+            data: nil,
+            progress: 0,
+            isIncoming: true,
+            createdAt: currentDate
+          )
+        ]
+      }
+      return db
+    }
+
+    let handler = ReceiveFileHandler.live(
+      messenger: messenger,
+      db: db,
+      now: { currentDate }
+    )
+
+    XCTAssertNoDifference(actions, [])
+
+    actions = []
+    let cancellable = handler(onError: { error in
+      actions.append(.didCatchError(error as NSError))
+    })
+
+    XCTAssertNoDifference(actions, [
+      .didRegisterReceiveFileCallback
+    ])
+
+    actions = []
+    let error = NSError(domain: "receive-file", code: 1)
+    receiveFileCallback?.handle(.failure(error))
+
+    XCTAssertNoDifference(actions, [
+      .didCatchError(error)
+    ])
+
+    actions = []
+    receiveFileCallback?.handle(.success(receivedFile))
+
+    XCTAssertNoDifference(actions, [
+      .didSaveFileTransfer(FileTransfer(
+        id: receivedFile.transferId,
+        contactId: receivedFile.senderId,
+        name: receivedFile.name,
+        type: receivedFile.type,
+        data: nil,
+        progress: 0,
+        isIncoming: true,
+        createdAt: currentDate
+      )),
+      .didSaveMessage(Message(
+        networkId: nil,
+        senderId: receivedFile.senderId,
+        recipientId: myContactId,
+        groupId: nil,
+        date: currentDate,
+        status: .received,
+        isUnread: false,
+        text: "",
+        replyMessageId: nil,
+        roundURL: nil,
+        fileTransferId: receivedFile.transferId
+      )),
+      .didReceiveFile(MessengerReceiveFile.Params(
+        transferId: receivedFile.transferId,
+        callbackIntervalMS: 500
+      )),
+    ])
+
+    actions = []
+    let receivingFileError = NSError(domain: "receiving-file", code: 2)
+    receivingFileCallback?(.failed(.receiveError(receivingFileError)))
+
+    XCTAssertNoDifference(actions, [
+      .didCatchError(receivingFileError)
+    ])
+
+    actions = []
+    let receivingFileCallbackError = NSError(domain: "receiving-file-callback", code: 3)
+    receivingFileCallback?(.failed(.callbackError(receivingFileCallbackError)))
+
+    XCTAssertNoDifference(actions, [
+      .didCatchError(receivingFileCallbackError)
+    ])
+
+    actions = []
+    let receivingFileProgressError = "receiving-file-progress"
+    receivingFileCallback?(.failed(.progressError(receivingFileProgressError)))
+
+    XCTAssertNoDifference(actions, [
+      .didCatchError(ReceiveFileHandler.ProgressError(message: receivingFileProgressError) as NSError)
+    ])
+
+    actions = []
+    receivingFileCallback?(.progress(transmitted: 1, total: 2))
+
+    XCTAssertNoDifference(actions, [
+      .didFetchFileTransfers(.init(id: [receivedFile.transferId])),
+      .didSaveFileTransfer(FileTransfer(
+        id: receivedFile.transferId,
+        contactId: receivedFile.senderId,
+        name: receivedFile.name,
+        type: receivedFile.type,
+        data: nil,
+        progress: 0.5,
+        isIncoming: true,
+        createdAt: currentDate
+      )),
+    ])
+
+    actions = []
+    let fileData = "file-data".data(using: .utf8)!
+    receivingFileCallback?(.finished(fileData))
+
+    XCTAssertNoDifference(actions, [
+      .didFetchFileTransfers(.init(id: [receivedFile.transferId])),
+      .didSaveFileTransfer(FileTransfer(
+        id: receivedFile.transferId,
+        contactId: receivedFile.senderId,
+        name: receivedFile.name,
+        type: receivedFile.type,
+        data: fileData,
+        progress: 1,
+        isIncoming: true,
+        createdAt: currentDate
+      )),
+    ])
+
+    actions = []
+    cancellable.cancel()
+
+    XCTAssertNoDifference(actions, [
+      .didCancelReceiveFileCallback
+    ])
+  }
+}
+
+private enum Action: Equatable {
+  case didRegisterReceiveFileCallback
+  case didCancelReceiveFileCallback
+  case didCatchError(NSError)
+  case didSaveFileTransfer(XXModels.FileTransfer)
+  case didSaveMessage(XXModels.Message)
+  case didReceiveFile(MessengerReceiveFile.Params)
+  case didFetchFileTransfers(XXModels.FileTransfer.Query)
+}
+
+private extension ReceivedFile {
+  static func stub() -> ReceivedFile {
+    ReceivedFile(
+      transferId: "received-file-transferId".data(using: .utf8)!,
+      senderId: "received-file-senderId".data(using: .utf8)!,
+      preview: "received-file-preview".data(using: .utf8)!,
+      name: "received-file-name",
+      type: "received-file-type",
+      size: 1234
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/SendImage/SendImageTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/SendImage/SendImageTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..d1cb1741abe2ffdbd2f4481800ca8529943cb9df
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/SendImage/SendImageTests.swift
@@ -0,0 +1,163 @@
+import CustomDump
+import XCTest
+import XXClient
+import XXMessengerClient
+import XXModels
+@testable import AppCore
+
+final class SendImageTests: XCTestCase {
+  func testSend() {
+    let image = "image-data".data(using: .utf8)!
+    let recipientId = "recipient-id".data(using: .utf8)!
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let transferId = "transfer-id".data(using: .utf8)!
+    let currentDate = Date(timeIntervalSince1970: 123)
+
+    var actions: [Action] = []
+    var sendFileCallback: MessengerSendFile.Callback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in myContactId }
+        return contact
+      }
+      return e2e
+    }
+    messenger.sendFile.run = { params, callback in
+      actions.append(.didSendFile(params))
+      sendFileCallback = callback
+      return transferId
+    }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveFileTransfer.run = { model in
+        actions.append(.didSaveFileTransfer(model))
+        return model
+      }
+      db.saveMessage.run = { model in
+        actions.append(.didSaveMessage(model))
+        return model
+      }
+      db.fetchFileTransfers.run = { query in
+        actions.append(.didFetchFileTransfers(query))
+        return [.stub(withProgress: 0)]
+      }
+      return db
+    }
+    let send: SendImage = .live(messenger: messenger, db: db, now: { currentDate })
+
+    actions = []
+    send(
+      image,
+      to: recipientId,
+      onError: { error in
+        actions.append(.didFail(error as NSError))
+      },
+      completion: {
+        actions.append(.didComplete)
+      }
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didSendFile(.init(
+        file: .init(
+          name: "image.jpg",
+          type: "image",
+          preview: nil,
+          contents: image
+        ),
+        recipientId: recipientId,
+        retry: 2,
+        callbackIntervalMS: 500
+      )),
+      .didSaveFileTransfer(.init(
+        id: transferId,
+        contactId: myContactId,
+        name: "image.jpg",
+        type: "image",
+        data: image,
+        progress: 0,
+        isIncoming: false,
+        createdAt: currentDate
+      )),
+      .didSaveMessage(.init(
+        senderId: myContactId,
+        recipientId: recipientId,
+        groupId: nil,
+        date: currentDate,
+        status: .sent,
+        isUnread: false,
+        text: "",
+        fileTransferId: transferId
+      )),
+    ])
+
+    actions = []
+    let sendError = NSError(domain: "send-error", code: 1)
+    sendFileCallback?(.failed(id: transferId, .error(sendError)))
+
+    XCTAssertNoDifference(actions, [
+      .didFail(sendError),
+    ])
+
+    actions = []
+    let closeError = NSError(domain: "close-error", code: 2)
+    sendFileCallback?(.failed(id: transferId, .close(closeError)))
+
+    XCTAssertNoDifference(actions, [
+      .didFail(closeError),
+    ])
+
+    actions = []
+    let progressError = "progress-error"
+    sendFileCallback?(.failed(id: transferId, .progressError(progressError)))
+
+    XCTAssertNoDifference(actions, [
+      .didFail(SendImage.ProgressError(message: progressError) as NSError),
+    ])
+
+    actions = []
+    sendFileCallback?(.progress(id: transferId, transmitted: 1, total: 2))
+
+    XCTAssertNoDifference(actions, [
+      .didFetchFileTransfers(.init(id: [transferId])),
+      .didSaveFileTransfer(.stub(withProgress: 0.5)),
+    ])
+
+    actions = []
+    sendFileCallback?(.finished(id: transferId))
+
+    XCTAssertNoDifference(actions, [
+      .didFetchFileTransfers(.init(id: [transferId])),
+      .didSaveFileTransfer(.stub(withProgress: 1)),
+    ])
+  }
+}
+
+private enum Action: Equatable {
+  case didFail(NSError)
+  case didComplete
+  case didSendFile(MessengerSendFile.Params)
+  case didSaveFileTransfer(XXModels.FileTransfer)
+  case didSaveMessage(XXModels.Message)
+  case didFetchFileTransfers(XXModels.FileTransfer.Query)
+}
+
+private extension XXModels.FileTransfer {
+  static func stub(withProgress progress: Float) -> XXModels.FileTransfer {
+    XXModels.FileTransfer(
+      id: Data(),
+      contactId: Data(),
+      name: "",
+      type: "",
+      data: nil,
+      progress: progress,
+      isIncoming: false,
+      createdAt: Date(timeIntervalSince1970: 0)
+    )
+  }
+}
diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
index 098a026c2c665c803b9b1ef00d4c6dbbe9250f0a..5ec16fc51cbb5f6b7ca0ce23696860b4460a8df3 100644
--- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift
@@ -34,6 +34,10 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.receiveFileHandler.run = { _ in
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {}
+    }
     store.environment.messenger.registerBackupCallback.run = { _ in
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
@@ -49,6 +53,7 @@ final class AppFeatureTests: XCTestCase {
       .didMakeDB,
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -83,6 +88,10 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.receiveFileHandler.run = { _ in
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {}
+    }
     store.environment.messenger.registerBackupCallback.run = { _ in
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
@@ -98,6 +107,7 @@ final class AppFeatureTests: XCTestCase {
       .didMakeDB,
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
@@ -132,6 +142,10 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.receiveFileHandler.run = { _ in
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {}
+    }
     store.environment.messenger.registerBackupCallback.run = { _ in
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
@@ -148,6 +162,7 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
@@ -182,6 +197,10 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.receiveFileHandler.run = { _ in
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {}
+    }
     store.environment.messenger.registerBackupCallback.run = { _ in
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
@@ -198,6 +217,7 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
       .didLoadMessenger,
     ])
@@ -229,6 +249,10 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.receiveFileHandler.run = { _ in
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {}
+    }
     store.environment.messenger.registerBackupCallback.run = { _ in
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
@@ -245,6 +269,7 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -331,6 +356,10 @@ final class AppFeatureTests: XCTestCase {
       actions.append(.didStartMessageListener)
       return Cancellable {}
     }
+    store.environment.receiveFileHandler.run = { _ in
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {}
+    }
     store.environment.messenger.registerBackupCallback.run = { _ in
       actions.append(.didRegisterBackupCallback)
       return Cancellable {}
@@ -346,6 +375,7 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -356,6 +386,7 @@ final class AppFeatureTests: XCTestCase {
     var actions: [Action]!
     var authHandlerOnError: [AuthCallbackHandler.OnError] = []
     var messageListenerOnError: [MessageListenerHandler.OnError] = []
+    var fileHandlerOnError: [ReceiveFileHandler.OnError] = []
     var backupCallback: [UpdateBackupFunc] = []
 
     let store = TestStore(
@@ -383,6 +414,13 @@ final class AppFeatureTests: XCTestCase {
         actions.append(.didCancelMessageListener)
       }
     }
+    store.environment.receiveFileHandler.run = { onError in
+      fileHandlerOnError.append(onError)
+      actions.append(.didStartReceiveFileHandler)
+      return Cancellable {
+        actions.append(.didCancelReceiveFileHandler)
+      }
+    }
     store.environment.messenger.registerBackupCallback.run = { callback in
       backupCallback.append(callback)
       actions.append(.didRegisterBackupCallback)
@@ -406,6 +444,7 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
     ])
 
@@ -420,28 +459,36 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didCancelAuthHandler,
       .didCancelMessageListener,
+      .didCancelReceiveFileHandler,
       .didCancelBackupCallback,
       .didStartAuthHandler,
       .didStartMessageListener,
+      .didStartReceiveFileHandler,
       .didRegisterBackupCallback,
     ])
 
     actions = []
-    struct AuthError: Error {}
-    let authError = AuthError()
+    let authError = NSError(domain: "auth-handler-error", code: 1)
     authHandlerOnError.first?(authError)
 
     XCTAssertNoDifference(actions, [
-      .didLog(.error(authError as NSError))
+      .didLog(.error(authError))
     ])
 
     actions = []
-    struct MessageError: Error {}
-    let messageError = MessageError()
+    let messageError = NSError(domain: "message-listener-error", code: 2)
     messageListenerOnError.first?(messageError)
 
     XCTAssertNoDifference(actions, [
-      .didLog(.error(messageError as NSError))
+      .didLog(.error(messageError))
+    ])
+
+    actions = []
+    let fileError = NSError(domain: "receive-file-error", code: 3)
+    fileHandlerOnError.first?(fileError)
+
+    XCTAssertNoDifference(actions, [
+      .didLog(.error(fileError))
     ])
 
     actions = []
@@ -458,6 +505,7 @@ final class AppFeatureTests: XCTestCase {
     XCTAssertNoDifference(actions, [
       .didCancelAuthHandler,
       .didCancelMessageListener,
+      .didCancelReceiveFileHandler,
       .didCancelBackupCallback,
     ])
   }
@@ -467,10 +515,12 @@ private enum Action: Equatable {
   case didMakeDB
   case didStartAuthHandler
   case didStartMessageListener
+  case didStartReceiveFileHandler
   case didRegisterBackupCallback
   case didLoadMessenger
   case didCancelAuthHandler
   case didCancelMessageListener
+  case didCancelReceiveFileHandler
   case didCancelBackupCallback
   case didLog(Logger.Message)
   case didStoreBackup(Data)
diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift
index 7f0633021b525d1e26517db1ffc87ed073fae3d6..01d0ed8faa3c37b0a2005ace0b7c98d13b2e6590 100644
--- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift
@@ -21,6 +21,8 @@ final class ChatFeatureTests: XCTestCase {
 
     var didFetchMessagesWithQuery: [XXModels.Message.Query] = []
     let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
+    var didFetchFileTransfersWithQuery: [XXModels.FileTransfer.Query] = []
+    let fileTransfersPublisher = PassthroughSubject<[XXModels.FileTransfer], Error>()
 
     store.environment.mainQueue = .immediate
     store.environment.bgQueue = .immediate
@@ -39,6 +41,10 @@ final class ChatFeatureTests: XCTestCase {
         didFetchMessagesWithQuery.append(query)
         return messagesPublisher.eraseToAnyPublisher()
       }
+      db.fetchFileTransfersPublisher.run = { query in
+        didFetchFileTransfersWithQuery.append(query)
+        return fileTransfersPublisher.eraseToAnyPublisher()
+      }
       return db
     }
 
@@ -49,7 +55,25 @@ final class ChatFeatureTests: XCTestCase {
     XCTAssertNoDifference(didFetchMessagesWithQuery, [
       .init(chat: .direct(myContactId, contactId))
     ])
+    XCTAssertNoDifference(didFetchFileTransfersWithQuery, [
+      .init(contactId: contactId, isIncoming: true),
+      .init(contactId: myContactId, isIncoming: false),
+    ])
 
+    let receivedFileTransfer = FileTransfer(
+      id: "file-transfer-1-id".data(using: .utf8)!,
+      contactId: contactId,
+      name: "file-transfer-1-name",
+      type: "file-transfer-1-type",
+      isIncoming: true
+    )
+    let sentFileTransfer = FileTransfer(
+      id: "file-transfer-2-id".data(using: .utf8)!,
+      contactId: myContactId,
+      name: "file-transfer-2-name",
+      type: "file-transfer-2-type",
+      isIncoming: false
+    )
     messagesPublisher.send([
       .init(
         id: nil,
@@ -69,7 +93,8 @@ final class ChatFeatureTests: XCTestCase {
         date: Date(timeIntervalSince1970: 1),
         status: .received,
         isUnread: false,
-        text: "Message 1"
+        text: "Message 1",
+        fileTransferId: receivedFileTransfer.id
       ),
       .init(
         id: 2,
@@ -79,9 +104,14 @@ final class ChatFeatureTests: XCTestCase {
         date: Date(timeIntervalSince1970: 2),
         status: .sent,
         isUnread: false,
-        text: "Message 2"
+        text: "Message 2",
+        fileTransferId: sentFileTransfer.id
       ),
     ])
+    fileTransfersPublisher.send([
+      receivedFileTransfer,
+      sentFileTransfer,
+    ])
 
     let expectedMessages = IdentifiedArrayOf<ChatState.Message>(uniqueElements: [
       .init(
@@ -89,14 +119,16 @@ final class ChatFeatureTests: XCTestCase {
         date: Date(timeIntervalSince1970: 1),
         senderId: contactId,
         text: "Message 1",
-        status: .received
+        status: .received,
+        fileTransfer: receivedFileTransfer
       ),
       .init(
         id: 2,
         date: Date(timeIntervalSince1970: 2),
         senderId: myContactId,
         text: "Message 2",
-        status: .sent
+        status: .sent,
+        fileTransfer: sentFileTransfer
       ),
     ])
 
@@ -105,6 +137,7 @@ final class ChatFeatureTests: XCTestCase {
     }
 
     messagesPublisher.send(completion: .finished)
+    fileTransfersPublisher.send(completion: .finished)
   }
 
   func testStartFailure() {
@@ -207,4 +240,70 @@ final class ChatFeatureTests: XCTestCase {
       $0.sendFailure = nil
     }
   }
+
+  func testSendImage() {
+    struct SendImageParams: Equatable {
+      var image: Data
+      var recipientId: Data
+    }
+    var didSendImageWithParams: [SendImageParams] = []
+    var sendImageCompletion: SendImage.Completion?
+
+    let store = TestStore(
+      initialState: ChatState(id: .contact("contact-id".data(using: .utf8)!)),
+      reducer: chatReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.sendImage.run = { image, recipientId, _, completion in
+      didSendImageWithParams.append(.init(image: image, recipientId: recipientId))
+      sendImageCompletion = completion
+    }
+
+    let image = "image-data".data(using: .utf8)!
+    store.send(.imagePicked(image))
+
+    XCTAssertNoDifference(didSendImageWithParams, [
+      .init(image: image, recipientId: "contact-id".data(using: .utf8)!)
+    ])
+
+    sendImageCompletion?()
+  }
+
+  func testSendImageFailure() {
+    var sendImageOnError: SendImage.OnError?
+    var sendImageCompletion: SendImage.Completion?
+
+    let store = TestStore(
+      initialState: ChatState(
+        id: .contact("contact-id".data(using: .utf8)!)
+      ),
+      reducer: chatReducer,
+      environment: .unimplemented
+    )
+
+    store.environment.mainQueue = .immediate
+    store.environment.bgQueue = .immediate
+    store.environment.sendImage.run = { _, _, onError, completion in
+      sendImageOnError = onError
+      sendImageCompletion = completion
+    }
+
+    store.send(.imagePicked(Data()))
+
+    let error = NSError(domain: "test", code: 123)
+    sendImageOnError?(error)
+
+    store.receive(.sendFailed(error.localizedDescription)) {
+      $0.sendFailure = error.localizedDescription
+    }
+
+    sendImageCompletion?()
+
+    store.send(.dismissSendFailureTapped) {
+      $0.sendFailure = nil
+    }
+  }
 }
diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
index 3a57414bb46df0058870adb5078aae2dfc41bf7c..6c23ff8b718191771ce8dbf30ef4777cfd7303f9 100644
--- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift
@@ -22,6 +22,7 @@ final class HomeFeatureTests: XCTestCase {
     var messengerDidStartWithTimeout: [Int] = []
     var messengerDidConnect = 0
     var messengerDidListenForMessages = 0
+    var messengerDidStartFileTransfer = 0
 
     store.environment.bgQueue = .immediate
     store.environment.mainQueue = .immediate
@@ -30,6 +31,8 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.connect.run = { messengerDidConnect += 1 }
     store.environment.messenger.isListeningForMessages.run = { false }
     store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 }
+    store.environment.messenger.isFileTransferRunning.run = { false }
+    store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 }
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { false }
 
@@ -38,6 +41,7 @@ final class HomeFeatureTests: XCTestCase {
     XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000])
     XCTAssertNoDifference(messengerDidConnect, 1)
     XCTAssertNoDifference(messengerDidListenForMessages, 1)
+    XCTAssertNoDifference(messengerDidStartFileTransfer, 1)
 
     store.receive(.networkMonitor(.stop))
     store.receive(.messenger(.didStartUnregistered)) {
@@ -55,6 +59,7 @@ final class HomeFeatureTests: XCTestCase {
     var messengerDidStartWithTimeout: [Int] = []
     var messengerDidConnect = 0
     var messengerDidListenForMessages = 0
+    var messengerDidStartFileTransfer = 0
     var messengerDidLogIn = 0
     var messengerDidResumeBackup = 0
 
@@ -65,6 +70,8 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.connect.run = { messengerDidConnect += 1 }
     store.environment.messenger.isListeningForMessages.run = { false }
     store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 }
+    store.environment.messenger.isFileTransferRunning.run = { false }
+    store.environment.messenger.startFileTransfer.run = { messengerDidStartFileTransfer += 1 }
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { true }
     store.environment.messenger.logIn.run = { messengerDidLogIn += 1 }
@@ -85,6 +92,7 @@ final class HomeFeatureTests: XCTestCase {
     XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000])
     XCTAssertNoDifference(messengerDidConnect, 1)
     XCTAssertNoDifference(messengerDidListenForMessages, 1)
+    XCTAssertNoDifference(messengerDidStartFileTransfer, 1)
     XCTAssertNoDifference(messengerDidLogIn, 1)
     XCTAssertNoDifference(messengerDidResumeBackup, 1)
 
@@ -112,6 +120,7 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) }
     store.environment.messenger.isConnected.run = { true }
     store.environment.messenger.isListeningForMessages.run = { true }
+    store.environment.messenger.isFileTransferRunning.run = { true }
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { true }
     store.environment.messenger.logIn.run = { messengerDidLogIn += 1 }
@@ -203,6 +212,7 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.start.run = { _ in }
     store.environment.messenger.isConnected.run = { true }
     store.environment.messenger.isListeningForMessages.run = { true }
+    store.environment.messenger.isFileTransferRunning.run = { true }
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { throw error }
 
@@ -229,6 +239,7 @@ final class HomeFeatureTests: XCTestCase {
     store.environment.messenger.start.run = { _ in }
     store.environment.messenger.isConnected.run = { true }
     store.environment.messenger.isListeningForMessages.run = { true }
+    store.environment.messenger.isFileTransferRunning.run = { true }
     store.environment.messenger.isLoggedIn.run = { false }
     store.environment.messenger.isRegistered.run = { true }
     store.environment.messenger.logIn.run = { throw error }
diff --git a/Sources/XXClient/Models/FileSend.swift b/Sources/XXClient/Models/FileSend.swift
index 1d925e6f4cd728b5fc364d53240c6ca61ceee4fa..8f95c675602101631240e3f74f4ec1878b2dcd20 100644
--- a/Sources/XXClient/Models/FileSend.swift
+++ b/Sources/XXClient/Models/FileSend.swift
@@ -4,7 +4,7 @@ public struct FileSend: Equatable {
   public init(
     name: String,
     type: String,
-    preview: Data,
+    preview: Data?,
     contents: Data
   ) {
     self.name = name
@@ -15,7 +15,7 @@ public struct FileSend: Equatable {
 
   public var name: String
   public var type: String
-  public var preview: Data
+  public var preview: Data?
   public var contents: Data
 }
 
diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsFileTransferRunning.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsFileTransferRunning.swift
new file mode 100644
index 0000000000000000000000000000000000000000..23a940802243ba95300f206285103adb9b7e73ad
--- /dev/null
+++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsFileTransferRunning.swift
@@ -0,0 +1,21 @@
+import XCTestDynamicOverlay
+
+public struct MessengerIsFileTransferRunning {
+  public var run: () -> Bool
+
+  public func callAsFunction() -> Bool {
+    run()
+  }
+}
+
+extension MessengerIsFileTransferRunning {
+  public static func live(_ env: MessengerEnvironment) -> MessengerIsFileTransferRunning {
+    MessengerIsFileTransferRunning { env.fileTransfer.get() != nil }
+  }
+}
+
+extension MessengerIsFileTransferRunning {
+  public static let unimplemented = MessengerIsFileTransferRunning(
+    run: XCTUnimplemented("\(Self.self)", placeholder: false)
+  )
+}
diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift
index d565ebb2b6515f00bacd776be88f83f7bcd7401c..a6f1ee0d3eb409f6c18fca2c0c62860a97721dfb 100644
--- a/Sources/XXMessengerClient/Messenger/Messenger.swift
+++ b/Sources/XXMessengerClient/Messenger/Messenger.swift
@@ -5,6 +5,7 @@ public struct Messenger {
   public var e2e: Stored<E2E?>
   public var ud: Stored<UserDiscovery?>
   public var backup: Stored<Backup?>
+  public var fileTransfer: Stored<FileTransfer?>
   public var isCreated: MessengerIsCreated
   public var create: MessengerCreate
   public var restoreBackup: MessengerRestoreBackup
@@ -41,6 +42,7 @@ public struct Messenger {
   public var setLogLevel: MessengerSetLogLevel
   public var startLogging: MessengerStartLogging
   public var registerReceiveFileCallback: MessengerRegisterReceiveFileCallback
+  public var isFileTransferRunning: MessengerIsFileTransferRunning
   public var startFileTransfer: MessengerStartFileTransfer
   public var sendFile: MessengerSendFile
   public var receiveFile: MessengerReceiveFile
@@ -53,6 +55,7 @@ extension Messenger {
       e2e: env.e2e,
       ud: env.ud,
       backup: env.backup,
+      fileTransfer: env.fileTransfer,
       isCreated: .live(env),
       create: .live(env),
       restoreBackup: .live(env),
@@ -89,6 +92,7 @@ extension Messenger {
       setLogLevel: .live(env),
       startLogging: .live(env),
       registerReceiveFileCallback: .live(env),
+      isFileTransferRunning: .live(env),
       startFileTransfer: .live(env),
       sendFile: .live(env),
       receiveFile: .live(env)
@@ -102,6 +106,7 @@ extension Messenger {
     e2e: .unimplemented(),
     ud: .unimplemented(),
     backup: .unimplemented(),
+    fileTransfer: .unimplemented(),
     isCreated: .unimplemented,
     create: .unimplemented,
     restoreBackup: .unimplemented,
@@ -138,6 +143,7 @@ extension Messenger {
     setLogLevel: .unimplemented,
     startLogging: .unimplemented,
     registerReceiveFileCallback: .unimplemented,
+    isFileTransferRunning: .unimplemented,
     startFileTransfer: .unimplemented,
     sendFile: .unimplemented,
     receiveFile: .unimplemented
diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsFileTransferRunningTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsFileTransferRunningTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..3ac1fd3008b4f3c0dec329a49351c0f540d67c4f
--- /dev/null
+++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsFileTransferRunningTests.swift
@@ -0,0 +1,20 @@
+import XCTest
+@testable import XXMessengerClient
+
+final class MessengerIsFileTransferRunningTests: XCTestCase {
+  func testIsRunning() {
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = { .unimplemented }
+    let isRunning: MessengerIsFileTransferRunning = .live(env)
+
+    XCTAssertTrue(isRunning())
+  }
+
+  func testIsNotRunning() {
+    var env: MessengerEnvironment = .unimplemented
+    env.fileTransfer.get = { nil }
+    let isRunning: MessengerIsFileTransferRunning = .live(env)
+
+    XCTAssertFalse(isRunning())
+  }
+}