diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index 4e09cabc4..a144f6df4 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -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) diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index c80508e17..dd12ea30d 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -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 { diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift index 6c4ee7982..4427c7075 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift @@ -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) diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift index 2a0488cc0..42d20c8ff 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenModels.swift @@ -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() } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift index 455f38d84..f59f5eee4 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift @@ -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)) } } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift index 2acb34f71..3d28a9145 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/View/MessageForwardingScreen.swift @@ -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) diff --git a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift index 3414b885b..d568b6654 100644 --- a/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift +++ b/UnitTests/Sources/MessageForwardingScreenViewModelTests.swift @@ -37,7 +37,7 @@ struct MessageForwardingScreenViewModelTests { @Test mutating func roomSelection() { context.send(viewAction: .selectRoom(roomID: "2")) - #expect(context.viewState.selectedRoomID == "2") + #expect(context.viewState.selectedRoomIDs == ["2"]) } @Test @@ -54,12 +54,12 @@ struct MessageForwardingScreenViewModelTests { @Test mutating func forwarding() async throws { context.send(viewAction: .selectRoom(roomID: "2")) - #expect(context.viewState.selectedRoomID == "2") + #expect(context.viewState.selectedRoomIDs == ["2"]) let deferred = deferFulfillment(viewModel.actions) { action in switch action { - case .sent(let roomID): - return roomID == "2" + case .sent(let roomIDs): + return roomIDs == ["2"] default: return false }