diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index be768aba1a18f015c4ceb79d0e5d97811c06ea6f..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
         )
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift
index 9e06c687da0ac5b02cffb06b21754514681f3ee2..3538bc2536f23e493a298f2761fd3614711f77da 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatFeature.swift
@@ -66,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>)
 }
@@ -75,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
   }
@@ -88,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>
 }
@@ -98,6 +102,7 @@ extension ChatEnvironment {
     messenger: .unimplemented,
     db: .unimplemented,
     sendMessage: .unimplemented,
+    sendImage: .unimplemented,
     mainQueue: .unimplemented,
     bgQueue: .unimplemented
   )
@@ -196,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 7c6c51f8ef4235d7d0777830cc8a4ac223ba1bf1..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()
         }
diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift
index 492d26d383857455b33dfa882851439c6c695973..01d0ed8faa3c37b0a2005ace0b7c98d13b2e6590 100644
--- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift
+++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift
@@ -240,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
+    }
+  }
 }