Allow the user to forward the same message to multiple rooms at once

This will send the item to the rooms in order they were chosen and only deep link
into the target room if single, otherwise it will just dismiss the forwarding screen
after sending.
This commit is contained in:
Stefan Ceriu
2026-05-27 13:55:35 +03:00
parent 7093d966b4
commit f040964d3f
7 changed files with 51 additions and 32 deletions
@@ -12,7 +12,7 @@ import Foundation
enum PinnedEventsTimelineFlowCoordinatorAction {
case finished
case displayUser(userID: String)
case forwardedMessageToRoom(roomID: String)
case forwardedMessageToRooms(roomIDs: [String])
case displayRoomScreenWithFocussedPin(eventID: String, threadRootEventID: String?)
}
@@ -148,9 +148,9 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .sent(let roomID):
case .sent(let roomIDs):
navigationStackCoordinator.setSheetCoordinator(nil)
actionsSubject.send(.forwardedMessageToRoom(roomID: roomID))
actionsSubject.send(.forwardedMessageToRooms(roomIDs: roomIDs))
}
}
.store(in: &cancellables)
@@ -1282,10 +1282,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .sent(let roomID):
case .sent(let roomIDs):
navigationStackCoordinator.setSheetCoordinator(nil)
// Timelines are cached - the local echo will be visible when fetching the room by its ID.
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
processPostMessageForwardingTo(rooms: roomIDs)
}
}
.store(in: &cancellables)
@@ -1524,6 +1523,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func processPostMessageForwardingTo(rooms roomIDs: [String]) {
// Only open forwarding target room if single.
guard roomIDs.count == 1, let roomID = roomIDs.last else {
return
}
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
}
// MARK: - Other flows
private func startChildFlow(for roomID: String, via: [String], entryPoint: RoomFlowCoordinatorEntryPoint) {
@@ -1582,9 +1590,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .displayUser(let userID):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.startMembersFlow(entryPoint: .roomMember(userID: userID)))
case .forwardedMessageToRoom(let roomID):
case .forwardedMessageToRooms(let roomIDs):
navigationStackCoordinator.setSheetCoordinator(nil)
stateMachine.tryEvent(.startChildFlow(roomID: roomID, via: [], entryPoint: .room))
processPostMessageForwardingTo(rooms: roomIDs)
case .displayRoomScreenWithFocussedPin(let eventID, let threadRootEventID):
navigationStackCoordinator.setSheetCoordinator(nil)
if let threadRootEventID {
@@ -18,7 +18,7 @@ struct MessageForwardingScreenCoordinatorParameters {
enum MessageForwardingScreenCoordinatorAction {
case dismiss
case sent(roomID: String)
case sent(roomIDs: [String])
}
final class MessageForwardingScreenCoordinator: CoordinatorProtocol {
@@ -42,8 +42,8 @@ final class MessageForwardingScreenCoordinator: CoordinatorProtocol {
switch action {
case .dismiss:
self?.actionsSubject.send(.dismiss)
case .sent(let roomID):
self?.actionsSubject.send(.sent(roomID: roomID))
case .sent(let roomIDs):
self?.actionsSubject.send(.sent(roomIDs: roomIDs))
}
}
.store(in: &cancellables)
@@ -11,12 +11,12 @@ import MatrixRustSDK
enum MessageForwardingScreenViewModelAction {
case dismiss
case sent(roomID: String)
case sent(roomIDs: [String])
}
struct MessageForwardingScreenViewState: BindableState {
var rooms: [MessageForwardingRoom] = []
var selectedRoomID: String?
var selectedRoomIDs = [String]()
var bindings = MessageForwardingScreenViewStateBindings()
}
@@ -63,7 +63,11 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
case .send:
Task { await forward() }
case .selectRoom(let roomID):
state.selectedRoomID = roomID
if let index = state.selectedRoomIDs.firstIndex(of: roomID) {
state.selectedRoomIDs.remove(at: index)
} else {
state.selectedRoomIDs.append(roomID)
}
case .reachedTop:
updateVisibleRange(edge: .top)
case .reachedBottom:
@@ -114,23 +118,30 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
}
private func forward() async {
guard let roomID = state.selectedRoomID else {
guard !state.selectedRoomIDs.isEmpty else {
fatalError()
}
guard case let .joined(targetRoomProxy) = await clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room to forward to with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
var succeededRoomIdentifiers = [String]()
for roomID in state.selectedRoomIDs {
guard case let .joined(targetRoomProxy) = await clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room to forward to with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
continue
}
if case .failure(let error) = await targetRoomProxy.timeline.sendMessageEventContent(forwardingItem.content) {
MXLog.error("Failed forwarding message with error: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
continue
}
succeededRoomIdentifiers.append(roomID)
}
if case .failure(let error) = await targetRoomProxy.timeline.sendMessageEventContent(forwardingItem.content) {
MXLog.error("Failed forwarding message with error: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
if !succeededRoomIdentifiers.isEmpty {
actionsSubject.send(.sent(roomIDs: succeededRoomIdentifiers))
}
// Timelines are cached - the local echo will be visible when fetching the room by its ID.
actionsSubject.send(.sent(roomID: roomID))
}
}
@@ -17,7 +17,7 @@ struct MessageForwardingScreen: View {
Section {
ForEach(context.viewState.rooms) { room in
MessageForwardingListRow(room: room,
isSelected: context.viewState.selectedRoomID == room.id,
isSelected: context.viewState.selectedRoomIDs.contains(room.id),
context: context)
}
// Replace these with ScrollView's `scrollPosition` when dropping iOS 16.
@@ -46,7 +46,7 @@ struct MessageForwardingScreen: View {
Button(L10n.actionSend) {
context.send(viewAction: .send)
}
.disabled(context.viewState.selectedRoomID == nil)
.disabled(context.viewState.selectedRoomIDs.isEmpty)
}
}
.searchController(query: $context.searchQuery, showsCancelButton: false)