diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift
index 2c22609f63e8b057bf780e2c47305c76f4f6c634..be183c099b55f8840c04f0470ab00cf3fd37ffd4 100644
--- a/Examples/xx-messenger/Package.swift
+++ b/Examples/xx-messenger/Package.swift
@@ -267,6 +267,7 @@ let package = Package(
       name: "GroupFeature",
       dependencies: [
         .target(name: "AppCore"),
+        .target(name: "ChatFeature"),
         .product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
         .product(name: "ComposablePresentation", package: "swift-composable-presentation"),
         .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"),
diff --git a/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift b/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift
index c67559af9d580952c14554465d356e27672b0e72..9e55ac6da53b79b6c43352a78f58c78e4020551d 100644
--- a/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift
+++ b/Examples/xx-messenger/Sources/AppCore/AppDependecies.swift
@@ -12,6 +12,7 @@ public struct AppDependencies {
   public var bgQueue: AnySchedulerOf<DispatchQueue>
   public var now: () -> Date
   public var sendMessage: SendMessage
+  public var sendGroupMessage: SendGroupMessage
   public var sendImage: SendImage
   public var messageListener: MessageListenerHandler
   public var receiveFileHandler: ReceiveFileHandler
@@ -46,6 +47,11 @@ extension AppDependencies {
         db: dbManager.getDB,
         now: now
       ),
+      sendGroupMessage: .live(
+        messenger: messenger,
+        db: dbManager.getDB,
+        now: now
+      ),
       sendImage: .live(
         messenger: messenger,
         db: dbManager.getDB,
@@ -85,6 +91,7 @@ extension AppDependencies {
       placeholder: Date(timeIntervalSince1970: 0)
     ),
     sendMessage: .unimplemented,
+    sendGroupMessage: .unimplemented,
     sendImage: .unimplemented,
     messageListener: .unimplemented,
     receiveFileHandler: .unimplemented,
diff --git a/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift b/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
new file mode 100644
index 0000000000000000000000000000000000000000..9427053788da3f869e9dd6b0845d581c7e8905c2
--- /dev/null
+++ b/Examples/xx-messenger/Sources/AppCore/SendMessage/SendGroupMessage.swift
@@ -0,0 +1,84 @@
+import Foundation
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+
+public struct SendGroupMessage {
+  public typealias OnError = (Error) -> Void
+  public typealias Completion = () -> Void
+
+  public var run: (String, Data, @escaping OnError, @escaping Completion) -> Void
+
+  public func callAsFunction(
+    text: String,
+    to groupId: Data,
+    onError: @escaping OnError,
+    completion: @escaping Completion
+  ) {
+    run(text, groupId, onError, completion)
+  }
+}
+
+extension SendGroupMessage {
+  public static func live(
+    messenger: Messenger,
+    db: DBManagerGetDB,
+    now: @escaping () -> Date
+  ) -> SendGroupMessage {
+    SendGroupMessage { text, groupId, onError, completion in
+      do {
+        let chat = try messenger.groupChat.tryGet()
+        let myContactId = try messenger.e2e.tryGet().getContact().getId()
+        var message = try db().saveMessage(.init(
+          senderId: myContactId,
+          recipientId: nil,
+          groupId: groupId,
+          date: now(),
+          status: .sending,
+          isUnread: false,
+          text: text
+        ))
+        let payload = MessagePayload(text: message.text)
+        let report = try chat.send(
+          groupId: groupId,
+          message: try payload.encode()
+        )
+        message.networkId = report.messageId
+        message.roundURL = report.roundURL
+        message = try db().saveMessage(message)
+        try messenger.cMix.tryGet().waitForRoundResult(
+          roundList: try report.encode(),
+          timeoutMS: 30_000,
+          callback: .init { result in
+            let status: XXModels.Message.Status
+            switch result {
+            case .delivered(_):
+              status = .sent
+            case .notDelivered(let timedOut):
+              status = timedOut ? .sendingTimedOut : .sendingFailed
+            }
+            do {
+              try db().bulkUpdateMessages(
+                .init(id: [message.id]),
+                .init(status: status)
+              )
+            } catch {
+              onError(error)
+            }
+            completion()
+          }
+        )
+      } catch {
+        onError(error)
+        completion()
+      }
+    }
+  }
+}
+
+extension SendGroupMessage {
+  public static let unimplemented = SendGroupMessage(
+    run: XCTUnimplemented("\(Self.self)")
+  )
+}
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift
index 6c71789dbbb201122cfda709999ac36984363b55..def3f14cdbae265ed27077c7e88a96416b924a49 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatComponent.swift
@@ -10,7 +10,8 @@ import XXModels
 public struct ChatComponent: ReducerProtocol {
   public struct State: Equatable, Identifiable {
     public enum ID: Equatable, Hashable {
-      case contact(Data)
+      case contact(XXModels.Contact.ID)
+      case group(XXModels.Group.ID)
     }
 
     public struct Message: Equatable, Identifiable {
@@ -18,6 +19,7 @@ public struct ChatComponent: ReducerProtocol {
         id: Int64,
         date: Date,
         senderId: Data,
+        senderName: String?,
         text: String,
         status: XXModels.Message.Status,
         fileTransfer: XXModels.FileTransfer? = nil
@@ -25,6 +27,7 @@ public struct ChatComponent: ReducerProtocol {
         self.id = id
         self.date = date
         self.senderId = senderId
+        self.senderName = senderName
         self.text = text
         self.status = status
         self.fileTransfer = fileTransfer
@@ -33,6 +36,7 @@ public struct ChatComponent: ReducerProtocol {
       public var id: Int64
       public var date: Date
       public var senderId: Data
+      public var senderName: String?
       public var text: String
       public var status: XXModels.Message.Status
       public var fileTransfer: XXModels.FileTransfer?
@@ -77,6 +81,7 @@ public struct ChatComponent: ReducerProtocol {
   @Dependency(\.app.messenger) var messenger: Messenger
   @Dependency(\.app.dbManager.getDB) var db: DBManagerGetDB
   @Dependency(\.app.sendMessage) var sendMessage: SendMessage
+  @Dependency(\.app.sendGroupMessage) var sendGroupMessage: SendGroupMessage
   @Dependency(\.app.sendImage) var sendImage: SendImage
   @Dependency(\.app.mainQueue) var mainQueue: AnySchedulerOf<DispatchQueue>
   @Dependency(\.app.bgQueue) var bgQueue: AnySchedulerOf<DispatchQueue>
@@ -93,37 +98,46 @@ public struct ChatComponent: ReducerProtocol {
           let myContactId = try messenger.e2e.tryGet().getContact().getId()
           state.myContactId = myContactId
           let queryChat: XXModels.Message.Query.Chat
-          let receivedFileTransfersQuery: XXModels.FileTransfer.Query
-          let sentFileTransfersQuery: XXModels.FileTransfer.Query
+          let receivedFileTransfersPublisher: AnyPublisher<[XXModels.FileTransfer], Error>
+          let sentFileTransfersPublisher: AnyPublisher<[XXModels.FileTransfer], Error>
           switch state.id {
           case .contact(let contactId):
             queryChat = .direct(myContactId, contactId)
-            receivedFileTransfersQuery = .init(
+            receivedFileTransfersPublisher = try db().fetchFileTransfersPublisher(.init(
               contactId: contactId,
               isIncoming: true
-            )
-            sentFileTransfersQuery = .init(
+            ))
+            sentFileTransfersPublisher = try db().fetchFileTransfersPublisher(.init(
               contactId: myContactId,
               isIncoming: false
-            )
+            ))
+          case .group(let groupId):
+            queryChat = .group(groupId)
+            receivedFileTransfersPublisher = Just([])
+              .setFailureType(to: Error.self)
+              .eraseToAnyPublisher()
+            sentFileTransfersPublisher = Just([])
+              .setFailureType(to: Error.self)
+              .eraseToAnyPublisher()
           }
           let messagesQuery = XXModels.Message.Query(chat: queryChat)
           return Publishers.CombineLatest3(
             try db().fetchMessagesPublisher(messagesQuery),
-            try db().fetchFileTransfersPublisher(receivedFileTransfersQuery),
-            try db().fetchFileTransfersPublisher(sentFileTransfersQuery)
+            try db().fetchContactsPublisher(.init()),
+            Publishers.CombineLatest(
+              receivedFileTransfersPublisher,
+              sentFileTransfersPublisher
+            ).map(+)
           )
-          .map { messages, receivedFileTransfers, sentFileTransfers in
-            (messages, receivedFileTransfers + sentFileTransfers)
-          }
           .assertNoFailure()
-          .map { messages, fileTransfers in
-            messages.compactMap { message in
+          .map { messages, contacts, fileTransfers -> [State.Message] in
+            messages.compactMap { message -> State.Message? in
               guard let id = message.id else { return nil }
               return State.Message(
                 id: id,
                 date: message.date,
                 senderId: message.senderId,
+                senderName: contacts.first { $0.id == message.senderId }?.username,
                 text: message.text,
                 status: message.status,
                 fileTransfer: fileTransfers.first { $0.id == message.fileTransferId }
@@ -163,6 +177,17 @@ public struct ChatComponent: ReducerProtocol {
                 subscriber.send(completion: .finished)
               }
             )
+          case .group(let groupId):
+            sendGroupMessage(
+              text: text,
+              to: groupId,
+              onError: { error in
+                subscriber.send(.sendFailed(error.localizedDescription))
+              },
+              completion: {
+                subscriber.send(completion: .finished)
+              }
+            )
           }
           return AnyCancellable {}
         }
@@ -175,21 +200,18 @@ public struct ChatComponent: ReducerProtocol {
         return .none
 
       case .imagePicked(let data):
-        let chatId = state.id
+        guard case .contact(let recipientId) = state.id else { return .none }
         return Effect.run { subscriber in
-          switch chatId {
-          case .contact(let recipientId):
-            sendImage(
-              data,
-              to: recipientId,
-              onError: { error in
-                subscriber.send(.sendFailed(error.localizedDescription))
-              },
-              completion: {
-                subscriber.send(completion: .finished)
-              }
-            )
-          }
+          sendImage(
+            data,
+            to: recipientId,
+            onError: { error in
+              subscriber.send(.sendFailed(error.localizedDescription))
+            },
+            completion: {
+              subscriber.send(completion: .finished)
+            }
+          )
           return AnyCancellable {}
         }
         .subscribe(on: bgQueue)
diff --git a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
index 1611815c087786954fd730fe1768760801a7ea73..a7272385a3241f5d46890a88039edefeb7b41d80 100644
--- a/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
+++ b/Examples/xx-messenger/Sources/ChatFeature/ChatView.swift
@@ -16,6 +16,7 @@ public struct ChatView: View {
     var failure: String?
     var sendFailure: String?
     var text: String
+    var disableImagePicker: Bool
 
     init(state: ChatComponent.State) {
       myContactId = state.myContactId
@@ -23,6 +24,12 @@ public struct ChatView: View {
       failure = state.failure
       sendFailure = state.sendFailure
       text = state.text
+      switch state.id {
+      case .contact(_):
+        disableImagePicker = false
+      case .group(_):
+        disableImagePicker = true
+      }
     }
   }
 
@@ -109,6 +116,7 @@ public struct ChatView: View {
                   }
                 }
               }
+              .disabled(viewStore.disableImagePicker)
             }
           }
           .padding()
@@ -139,6 +147,13 @@ public struct ChatView: View {
 
     var body: some View {
       VStack {
+        if let sender = message.senderName {
+          Text(sender)
+            .foregroundColor(.secondary)
+            .font(.footnote)
+            .frame(maxWidth: .infinity, alignment: alignment)
+        }
+
         Text("\(message.date.formatted()), \(statusText)")
           .foregroundColor(.secondary)
           .font(.footnote)
@@ -208,6 +223,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 1,
               date: Date(),
               senderId: "contact-id".data(using: .utf8)!,
+              senderName: "Contact",
               text: "Hello!",
               status: .received
             ),
@@ -215,6 +231,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 2,
               date: Date(),
               senderId: "my-contact-id".data(using: .utf8)!,
+              senderName: "Me",
               text: "Hi!",
               status: .sent
             ),
@@ -222,6 +239,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 3,
               date: Date(),
               senderId: "contact-id".data(using: .utf8)!,
+              senderName: "Contact",
               text: "",
               status: .received,
               fileTransfer: .init(
@@ -237,6 +255,7 @@ public struct ChatView_Previews: PreviewProvider {
               id: 4,
               date: Date(),
               senderId: "my-contact-id".data(using: .utf8)!,
+              senderName: "Me",
               text: "",
               status: .sent,
               fileTransfer: .init(
diff --git a/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift b/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift
index d9b7d23306b64b6924d5ce7ff3351728667476a3..359ee8991596e56cac72c39626955366588033b0 100644
--- a/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift
+++ b/Examples/xx-messenger/Sources/GroupFeature/GroupComponent.swift
@@ -1,5 +1,7 @@
 import AppCore
+import ChatFeature
 import ComposableArchitecture
+import ComposablePresentation
 import Foundation
 import XXMessengerClient
 import XXModels
@@ -10,18 +12,21 @@ public struct GroupComponent: ReducerProtocol {
       groupId: XXModels.Group.ID,
       groupInfo: XXModels.GroupInfo? = nil,
       isJoining: Bool = false,
-      joinFailure: String? = nil
+      joinFailure: String? = nil,
+      chat: ChatComponent.State? = nil
     ) {
       self.groupId = groupId
       self.groupInfo = groupInfo
       self.isJoining = isJoining
       self.joinFailure = joinFailure
+      self.chat = chat
     }
 
     public var groupId: XXModels.Group.ID
     public var groupInfo: XXModels.GroupInfo?
     public var isJoining: Bool
     public var joinFailure: String?
+    public var chat: ChatComponent.State?
   }
 
   public enum Action: Equatable {
@@ -30,6 +35,9 @@ public struct GroupComponent: ReducerProtocol {
     case joinButtonTapped
     case didJoin
     case didFailToJoin(String)
+    case chatButtonTapped
+    case didDismissChat
+    case chat(ChatComponent.Action)
   }
 
   public init() {}
@@ -88,7 +96,24 @@ public struct GroupComponent: ReducerProtocol {
         state.isJoining = false
         state.joinFailure = failure
         return .none
+
+      case .chatButtonTapped:
+        state.chat = ChatComponent.State(id: .group(state.groupId))
+        return .none
+
+      case .didDismissChat:
+        state.chat = nil
+        return .none
+
+      case .chat(_):
+        return .none
       }
     }
+    .presenting(
+      state: .keyPath(\.chat),
+      id: .notNil(),
+      action: /Action.chat,
+      presented: { ChatComponent() }
+    )
   }
 }
diff --git a/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift b/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift
index 67b880f4b1ad3f8e790f51f1202624f195d0d60a..a7520d53802b8fc8a31a306a4305804b94c1cb61 100644
--- a/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift
+++ b/Examples/xx-messenger/Sources/GroupFeature/GroupView.swift
@@ -1,5 +1,7 @@
 import AppCore
+import ChatFeature
 import ComposableArchitecture
+import ComposablePresentation
 import SwiftUI
 import XXModels
 
@@ -68,8 +70,28 @@ public struct GroupView: View {
             }
           }
         }
+
+        Section {
+          Button {
+            viewStore.send(.chatButtonTapped)
+          } label: {
+            HStack {
+              Text("Chat")
+              Spacer()
+              Image(systemName: "chevron.forward")
+            }
+          }
+        }
       }
       .navigationTitle("Group")
+      .background(NavigationLinkWithStore(
+        store.scope(
+          state: \.chat,
+          action: Component.Action.chat
+        ),
+        onDeactivate: { viewStore.send(.didDismissChat) },
+        destination: ChatView.init(store:)
+      ))
       .task { viewStore.send(.start) }
     }
   }
diff --git a/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
new file mode 100644
index 0000000000000000000000000000000000000000..1df2eaa3717344509c590a462036218c0ff416b0
--- /dev/null
+++ b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendGroupMessageTests.swift
@@ -0,0 +1,279 @@
+import CustomDump
+import XCTest
+import XCTestDynamicOverlay
+import XXClient
+import XXMessengerClient
+import XXModels
+@testable import AppCore
+
+final class SendGroupMessageTests: XCTestCase {
+  enum Action: Equatable {
+    case didReceiveError(String)
+    case didComplete
+    case didSaveMessage(XXModels.Message)
+    case didSend(groupId: Data, message: Data, tag: String?)
+    case didWaitForRoundResults(roundList: Data, timeoutMS: Int)
+    case didUpdateMessage(
+      query: XXModels.Message.Query,
+      assignments: XXModels.Message.Assignments
+    )
+  }
+
+  var actions: [Action]!
+
+  override func setUp() {
+    actions = []
+  }
+
+  override func tearDown() {
+    actions = nil
+  }
+
+  func testSend() {
+    let text = "Hello!"
+    let groupId = "group-id".data(using: .utf8)!
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let messageId: Int64 = 321
+    let sendReport = GroupSendReport(
+      rounds: [],
+      roundURL: "round-url",
+      timestamp: 1234,
+      messageId: "message-id".data(using: .utf8)!
+    )
+
+    var messageDeliveryCallback: MessageDeliveryCallback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.send.run = { groupId, message, tag in
+        self.actions.append(.didSend(groupId: groupId, message: message, tag: tag))
+        return sendReport
+      }
+      return groupChat
+    }
+    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.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForRoundResult.run = { roundList, timeoutMS, callback in
+        self.actions.append(.didWaitForRoundResults(roundList: roundList, timeoutMS: timeoutMS))
+        messageDeliveryCallback = callback
+      }
+      return cMix
+    }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveMessage.run = { message in
+        self.actions.append(.didSaveMessage(message))
+        var message = message
+        message.id = messageId
+        return message
+      }
+      db.bulkUpdateMessages.run = { query, assignments in
+        self.actions.append(.didUpdateMessage(query: query, assignments: assignments))
+        return 1
+      }
+      return db
+    }
+    let now = Date()
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: { now }
+    )
+
+    send(
+      text: text,
+      to: groupId,
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didSaveMessage(.init(
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: now,
+        status: .sending,
+        isUnread: false,
+        text: text
+      )),
+      .didSend(
+        groupId: groupId,
+        message: try! MessagePayload(text: text).encode(),
+        tag: nil
+      ),
+      .didSaveMessage(.init(
+        id: messageId,
+        networkId: sendReport.messageId,
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: now,
+        status: .sending,
+        isUnread: false,
+        text: text,
+        roundURL: sendReport.roundURL
+      )),
+      .didWaitForRoundResults(
+        roundList: try! sendReport.encode(),
+        timeoutMS: 30_000
+      ),
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.delivered(roundResults: []))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sent)
+      ),
+      .didComplete,
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.notDelivered(timedOut: true))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sendingTimedOut)
+      ),
+      .didComplete,
+    ])
+
+    actions = []
+    messageDeliveryCallback?.handle(.notDelivered(timedOut: false))
+
+    XCTAssertNoDifference(actions, [
+      .didUpdateMessage(
+        query: .init(id: [messageId]),
+        assignments: .init(status: .sendingFailed)
+      ),
+      .didComplete,
+    ])
+  }
+
+  func testSendDatabaseFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var messenger: Messenger = .unimplemented
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in Data() }
+        return contact
+      }
+      return e2e
+    }
+    messenger.groupChat.get = { .unimplemented }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = { throw failure }
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: XCTestDynamicOverlay.unimplemented("now", placeholder: Date())
+    )
+
+    send(
+      text: "Hello",
+      to: "group-id".data(using: .utf8)!,
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    XCTAssertNoDifference(actions, [
+      .didReceiveError(failure.localizedDescription),
+      .didComplete
+    ])
+  }
+
+  func testBulkUpdateOnDeliveryFailure() {
+    struct Failure: Error, Equatable {}
+    let failure = Failure()
+
+    var messageDeliveryCallback: MessageDeliveryCallback?
+
+    var messenger: Messenger = .unimplemented
+    messenger.groupChat.get = {
+      var groupChat: GroupChat = .unimplemented
+      groupChat.send.run = { _, _, _ in
+        GroupSendReport(
+          rounds: [],
+          roundURL: "",
+          timestamp: 0,
+          messageId: Data()
+        )
+      }
+      return groupChat
+    }
+    messenger.e2e.get = {
+      var e2e: E2E = .unimplemented
+      e2e.getContact.run = {
+        var contact: XXClient.Contact = .unimplemented(Data())
+        contact.getIdFromContact.run = { _ in Data() }
+        return contact
+      }
+      return e2e
+    }
+    messenger.cMix.get = {
+      var cMix: CMix = .unimplemented
+      cMix.waitForRoundResult.run = { _, _, callback in
+        messageDeliveryCallback = callback
+      }
+      return cMix
+    }
+    var db: DBManagerGetDB = .unimplemented
+    db.run = {
+      var db: Database = .unimplemented
+      db.saveMessage.run = { message in message }
+      db.bulkUpdateMessages.run = { _, _ in throw failure }
+      return db
+    }
+    let now = Date()
+    let send: SendGroupMessage = .live(
+      messenger: messenger,
+      db: db,
+      now: { now }
+    )
+
+    send(
+      text: "Hello",
+      to: Data(),
+      onError: { error in
+        self.actions.append(.didReceiveError(error.localizedDescription))
+      },
+      completion: {
+        self.actions.append(.didComplete)
+      }
+    )
+
+    messageDeliveryCallback?.handle(.delivered(roundResults: []))
+
+    XCTAssertNoDifference(actions, [
+      .didReceiveError(failure.localizedDescription),
+      .didComplete,
+    ])
+  }
+}
diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift
index c8f5952864c6f77f04ec42b53b5875a317ee37a4..ba7ed23792f71c169b42daffc10fec988caef3a5 100644
--- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift
+++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatComponentTests.swift
@@ -9,7 +9,7 @@ import XXModels
 @testable import ChatFeature
 
 final class ChatComponentTests: XCTestCase {
-  func testStart() {
+  func testStartDirectChat() {
     let contactId = "contact-id".data(using: .utf8)!
     let myContactId = "my-contact-id".data(using: .utf8)!
 
@@ -22,6 +22,8 @@ final class ChatComponentTests: XCTestCase {
     let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
     var didFetchFileTransfersWithQuery: [XXModels.FileTransfer.Query] = []
     let fileTransfersPublisher = PassthroughSubject<[XXModels.FileTransfer], Error>()
+    var didFetchContactsWithQuery: [XXModels.Contact.Query] = []
+    let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
 
     store.dependencies.app.mainQueue = .immediate
     store.dependencies.app.bgQueue = .immediate
@@ -40,6 +42,10 @@ final class ChatComponentTests: XCTestCase {
         didFetchMessagesWithQuery.append(query)
         return messagesPublisher.eraseToAnyPublisher()
       }
+      db.fetchContactsPublisher.run = { query in
+        didFetchContactsWithQuery.append(query)
+        return contactsPublisher.eraseToAnyPublisher()
+      }
       db.fetchFileTransfersPublisher.run = { query in
         didFetchFileTransfersWithQuery.append(query)
         return fileTransfersPublisher.eraseToAnyPublisher()
@@ -58,6 +64,9 @@ final class ChatComponentTests: XCTestCase {
       .init(contactId: contactId, isIncoming: true),
       .init(contactId: myContactId, isIncoming: false),
     ])
+    XCTAssertNoDifference(didFetchContactsWithQuery, [
+      .init(),
+    ])
 
     let receivedFileTransfer = FileTransfer(
       id: "file-transfer-1-id".data(using: .utf8)!,
@@ -111,12 +120,17 @@ final class ChatComponentTests: XCTestCase {
       receivedFileTransfer,
       sentFileTransfer,
     ])
+    contactsPublisher.send([
+      .init(id: myContactId, username: "My username"),
+      .init(id: contactId, username: "Contact username"),
+    ])
 
     let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [
       .init(
         id: 1,
         date: Date(timeIntervalSince1970: 1),
         senderId: contactId,
+        senderName: "Contact username",
         text: "Message 1",
         status: .received,
         fileTransfer: receivedFileTransfer
@@ -125,6 +139,7 @@ final class ChatComponentTests: XCTestCase {
         id: 2,
         date: Date(timeIntervalSince1970: 2),
         senderId: myContactId,
+        senderName: "My username",
         text: "Message 2",
         status: .sent,
         fileTransfer: sentFileTransfer
@@ -137,6 +152,131 @@ final class ChatComponentTests: XCTestCase {
 
     messagesPublisher.send(completion: .finished)
     fileTransfersPublisher.send(completion: .finished)
+    contactsPublisher.send(completion: .finished)
+  }
+
+  func testStartGroupChat() {
+    let groupId = "group-id".data(using: .utf8)!
+    let myContactId = "my-contact-id".data(using: .utf8)!
+    let firstMemberId = "member-1-id".data(using: .utf8)!
+    let secondMemberId = "member-2-id".data(using: .utf8)!
+
+    let store = TestStore(
+      initialState: ChatComponent.State(id: .group(groupId)),
+      reducer: ChatComponent()
+    )
+
+    var didFetchMessagesWithQuery: [XXModels.Message.Query] = []
+    let messagesPublisher = PassthroughSubject<[XXModels.Message], Error>()
+    var didFetchContactsWithQuery: [XXModels.Contact.Query] = []
+    let contactsPublisher = PassthroughSubject<[XXModels.Contact], Error>()
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.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
+    }
+    store.dependencies.app.dbManager.getDB.run = {
+      var db: Database = .unimplemented
+      db.fetchMessagesPublisher.run = { query in
+        didFetchMessagesWithQuery.append(query)
+        return messagesPublisher.eraseToAnyPublisher()
+      }
+      db.fetchContactsPublisher.run = { query in
+        didFetchContactsWithQuery.append(query)
+        return contactsPublisher.eraseToAnyPublisher()
+      }
+      return db
+    }
+
+    store.send(.start) {
+      $0.myContactId = myContactId
+    }
+
+    XCTAssertNoDifference(didFetchMessagesWithQuery, [
+      .init(chat: .group(groupId))
+    ])
+    XCTAssertNoDifference(didFetchContactsWithQuery, [
+      .init(),
+    ])
+
+    messagesPublisher.send([
+      .init(
+        id: 0,
+        senderId: myContactId,
+        recipientId: nil,
+        groupId: groupId,
+        date: Date(timeIntervalSince1970: 0),
+        status: .sent,
+        isUnread: false,
+        text: "Message 0"
+      ),
+      .init(
+        id: 1,
+        senderId: firstMemberId,
+        recipientId: nil,
+        groupId: groupId,
+        date: Date(timeIntervalSince1970: 1),
+        status: .received,
+        isUnread: false,
+        text: "Message 1"
+      ),
+      .init(
+        id: 2,
+        senderId: secondMemberId,
+        recipientId: nil,
+        groupId: groupId,
+        date: Date(timeIntervalSince1970: 2),
+        status: .received,
+        isUnread: false,
+        text: "Message 2"
+      ),
+    ])
+    contactsPublisher.send([
+      .init(id: myContactId, username: "My username"),
+      .init(id: firstMemberId, username: "First username"),
+      .init(id: secondMemberId, username: "Second username"),
+    ])
+
+    let expectedMessages = IdentifiedArrayOf<ChatComponent.State.Message>(uniqueElements: [
+      .init(
+        id: 0,
+        date: Date(timeIntervalSince1970: 0),
+        senderId: myContactId,
+        senderName: "My username",
+        text: "Message 0",
+        status: .sent
+      ),
+      .init(
+        id: 1,
+        date: Date(timeIntervalSince1970: 1),
+        senderId: firstMemberId,
+        senderName: "First username",
+        text: "Message 1",
+        status: .received
+      ),
+      .init(
+        id: 2,
+        date: Date(timeIntervalSince1970: 2),
+        senderId: secondMemberId,
+        senderName: "Second username",
+        text: "Message 2",
+        status: .received
+      ),
+    ])
+
+    store.receive(.didFetchMessages(expectedMessages)) {
+      $0.messages = expectedMessages
+    }
+
+    messagesPublisher.send(completion: .finished)
+    contactsPublisher.send(completion: .finished)
   }
 
   func testStartFailure() {
@@ -165,7 +305,7 @@ final class ChatComponentTests: XCTestCase {
     }
   }
 
-  func testSend() {
+  func testSendDirectMessage() {
     struct SendMessageParams: Equatable {
       var text: String
       var recipientId: Data
@@ -200,7 +340,7 @@ final class ChatComponentTests: XCTestCase {
     sendMessageCompletion?()
   }
 
-  func testSendFailure() {
+  func testSendDirectMessageFailure() {
     var sendMessageOnError: SendMessage.OnError?
     var sendMessageCompletion: SendMessage.Completion?
 
@@ -237,6 +377,80 @@ final class ChatComponentTests: XCTestCase {
     }
   }
 
+  func testSendGroupMessage() {
+    let groupId = "group-id".data(using: .utf8)!
+    let text = "Hello"
+    struct SendGroupMessageParams: Equatable {
+      var text: String
+      var groupId: Data
+    }
+    var didSendGroupMessageWithParams: [SendGroupMessageParams] = []
+    var sendGroupMessageCompletion: SendGroupMessage.Completion?
+
+    let store = TestStore(
+      initialState: ChatComponent.State(id: .group(groupId)),
+      reducer: ChatComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.sendGroupMessage.run = { text, groupId, _, completion in
+      didSendGroupMessageWithParams.append(.init(text: text, groupId: groupId))
+      sendGroupMessageCompletion = completion
+    }
+
+    store.send(.set(\.$text, text)) {
+      $0.text = text
+    }
+
+    store.send(.sendTapped) {
+      $0.text = ""
+    }
+
+    XCTAssertNoDifference(didSendGroupMessageWithParams, [
+      .init(text: text, groupId: groupId)
+    ])
+
+    sendGroupMessageCompletion?()
+  }
+
+  func testSendGroupMessageFailure() {
+    var sendGroupMessageOnError: SendGroupMessage.OnError?
+    var sendGroupMessageCompletion: SendGroupMessage.Completion?
+
+    let store = TestStore(
+      initialState: ChatComponent.State(
+        id: .group("group-id".data(using: .utf8)!),
+        text: "Hello"
+      ),
+      reducer: ChatComponent()
+    )
+
+    store.dependencies.app.mainQueue = .immediate
+    store.dependencies.app.bgQueue = .immediate
+    store.dependencies.app.sendGroupMessage.run = { _, _, onError, completion in
+      sendGroupMessageOnError = onError
+      sendGroupMessageCompletion = completion
+    }
+
+    store.send(.sendTapped) {
+      $0.text = ""
+    }
+
+    let error = NSError(domain: "test", code: 123)
+    sendGroupMessageOnError?(error)
+
+    store.receive(.sendFailed(error.localizedDescription)) {
+      $0.sendFailure = error.localizedDescription
+    }
+
+    sendGroupMessageCompletion?()
+
+    store.send(.dismissSendFailureTapped) {
+      $0.sendFailure = nil
+    }
+  }
+
   func testSendImage() {
     struct SendImageParams: Equatable {
       var image: Data
diff --git a/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift b/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift
index 3c7193fc4ebea418f97800fae37e782d982d497f..b3791b1f107106d9935e6576e664f9ad40cf80f7 100644
--- a/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift
+++ b/Examples/xx-messenger/Tests/GroupFeatureTests/GroupComponentTests.swift
@@ -1,3 +1,4 @@
+import ChatFeature
 import Combine
 import ComposableArchitecture
 import CustomDump
@@ -140,6 +141,26 @@ final class GroupComponentTests: XCTestCase {
       $0.joinFailure = failure.localizedDescription
     }
   }
+
+  func testPresentChat() {
+    let groupInfo = GroupInfo.stub()
+
+    let store = TestStore(
+      initialState: GroupComponent.State(
+        groupId: groupInfo.group.id,
+        groupInfo: groupInfo
+      ),
+      reducer: GroupComponent()
+    )
+
+    store.send(.chatButtonTapped) {
+      $0.chat = ChatComponent.State(id: .group(groupInfo.id))
+    }
+
+    store.send(.didDismissChat) {
+      $0.chat = nil
+    }
+  }
 }
 
 private extension XXModels.GroupInfo {