Move message forwarding out of the flow coordinator. (#2738)

This commit is contained in:
Doug 2024-05-07 15:04:11 +01:00 committed by GitHub
parent eac2552ab5
commit e829a3ded6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 140 additions and 92 deletions

View File

@ -303,8 +303,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.emojiPicker, .dismissEmojiPicker):
return .room
case (.room, .presentMessageForwarding(let itemID)):
return .messageForwarding(itemID: itemID)
case (.room, .presentMessageForwarding(let forwardingItem)):
return .messageForwarding(forwardingItem: forwardingItem)
case (.messageForwarding, .dismissMessageForwarding):
return .room
@ -435,8 +435,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.emojiPicker, .dismissEmojiPicker, .room):
break
case (.room, .presentMessageForwarding(let itemID), .messageForwarding):
presentMessageForwarding(for: itemID)
case (.room, .presentMessageForwarding(let forwardingItem), .messageForwarding):
presentMessageForwarding(with: forwardingItem)
case (.messageForwarding, .dismissMessageForwarding, .room):
break
@ -579,8 +579,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentMapNavigator(interactionMode: .viewOnly(geoURI: geoURI, description: description)))
case .presentRoomMemberDetails(userID: let userID):
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
case .presentMessageForwarding(let itemID):
stateMachine.tryEvent(.presentMessageForwarding(itemID: itemID))
case .presentMessageForwarding(let forwardingItem):
stateMachine.tryEvent(.presentMessageForwarding(forwardingItem: forwardingItem))
case .presentCallScreen:
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
}
@ -1100,16 +1100,18 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider, let eventID = itemID.eventID else {
private func presentMessageForwarding(with forwardingItem: MessageForwardingItem) {
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider else {
fatalError()
}
let stackCoordinator = NavigationStackCoordinator()
let parameters = MessageForwardingScreenCoordinatorParameters(roomSummaryProvider: roomSummaryProvider,
let parameters = MessageForwardingScreenCoordinatorParameters(forwardingItem: forwardingItem,
clientProxy: userSession.clientProxy,
roomSummaryProvider: roomSummaryProvider,
mediaProvider: userSession.mediaProvider,
sourceRoomID: roomProxy.id)
userIndicatorController: userIndicatorController)
let coordinator = MessageForwardingScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
@ -1118,12 +1120,10 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .send(let roomID):
case .sent(let roomID):
navigationStackCoordinator.setSheetCoordinator(nil)
Task {
await self.forward(eventID: eventID, toRoomID: roomID)
}
// Timelines are cached - the local echo will be visible when fetching the room by its ID.
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room))
}
}
.store(in: &cancellables)
@ -1135,30 +1135,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func forward(eventID: String, toRoomID roomID: String) async {
guard let messageEventContent = await roomProxy.timeline.messageEventContent(for: eventID) else {
MXLog.error("Failed retrieving forwarded message event content for eventID: \(eventID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
guard let targetRoomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room to forward to with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
if case .failure(let error) = await targetRoomProxy.timeline.sendMessageEventContent(messageEventContent) {
MXLog.error("Failed forwarding message with error: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
// We don't need to worry about passing in the room proxy as timelines are
// cached. The local echo will be visible when fetching the room by its ID.
stateMachine.tryEvent(.startChildFlow(roomID: roomID, entryPoint: .room))
}
private func presentNotificationSettingsScreen() {
let parameters = RoomNotificationSettingsScreenCoordinatorParameters(notificationSettingsProxy: userSession.clientProxy.notificationSettings,
roomProxy: roomProxy,
@ -1356,7 +1332,7 @@ private extension RoomFlowCoordinator {
case mediaUploadPreview(fileURL: URL)
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case mapNavigator
case messageForwarding(itemID: TimelineItemIdentifier)
case messageForwarding(forwardingItem: MessageForwardingItem)
case reportContent(itemID: TimelineItemIdentifier, senderID: String)
case pollForm
case pollsHistory
@ -1419,7 +1395,7 @@ private extension RoomFlowCoordinator {
case presentMapNavigator(interactionMode: StaticLocationInteractionMode)
case dismissMapNavigator
case presentMessageForwarding(itemID: TimelineItemIdentifier)
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
case dismissMessageForwarding
case presentPollForm(mode: PollFormMode)

View File

@ -40,14 +40,15 @@ struct RoomProxyMockConfiguration {
var canUserJoinCall = true
func makeTimeline() -> TimelineProxyMock {
let mock = TimelineProxyMock()
mock.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
let timeline = TimelineProxyMock()
timeline.underlyingActions = Empty(completeImmediately: false).eraseToAnyPublisher()
timeline.sendMessageEventContentReturnValue = .success(())
let timelineProvider = RoomTimelineProviderMock()
timelineProvider.paginationState = .init(backward: timelineStartReached ? .timelineEndReached : .idle, forward: .timelineEndReached)
timelineProvider.underlyingMembershipChangePublisher = PassthroughSubject().eraseToAnyPublisher()
mock.underlyingTimelineProvider = timelineProvider
return mock
timeline.underlyingTimelineProvider = timelineProvider
return timeline
}
}

View File

@ -18,14 +18,16 @@ import Combine
import SwiftUI
struct MessageForwardingScreenCoordinatorParameters {
let forwardingItem: MessageForwardingItem
let clientProxy: ClientProxyProtocol
let roomSummaryProvider: RoomSummaryProviderProtocol
let mediaProvider: MediaProviderProtocol
let sourceRoomID: String
let userIndicatorController: UserIndicatorControllerProtocol
}
enum MessageForwardingScreenCoordinatorAction {
case dismiss
case send(roomID: String)
case sent(roomID: String)
}
final class MessageForwardingScreenCoordinator: CoordinatorProtocol {
@ -38,9 +40,11 @@ final class MessageForwardingScreenCoordinator: CoordinatorProtocol {
}
init(parameters: MessageForwardingScreenCoordinatorParameters) {
viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: parameters.roomSummaryProvider,
mediaProvider: parameters.mediaProvider,
sourceRoomID: parameters.sourceRoomID)
viewModel = MessageForwardingScreenViewModel(forwardingItem: parameters.forwardingItem,
clientProxy: parameters.clientProxy,
roomSummaryProvider: parameters.roomSummaryProvider,
userIndicatorController: parameters.userIndicatorController,
mediaProvider: parameters.mediaProvider)
}
func start() {
@ -48,8 +52,8 @@ final class MessageForwardingScreenCoordinator: CoordinatorProtocol {
switch action {
case .dismiss:
self?.actionsSubject.send(.dismiss)
case .send(let roomID):
self?.actionsSubject.send(.send(roomID: roomID))
case .sent(let roomID):
self?.actionsSubject.send(.sent(roomID: roomID))
}
}
.store(in: &cancellables)

View File

@ -15,10 +15,11 @@
//
import Foundation
import MatrixRustSDK
enum MessageForwardingScreenViewModelAction {
case dismiss
case send(roomID: String)
case sent(roomID: String)
}
struct MessageForwardingScreenViewState: BindableState {
@ -45,3 +46,21 @@ struct MessageForwardingRoom: Identifiable, Equatable {
let alias: String?
let avatarURL: URL?
}
struct MessageForwardingItem: Hashable {
/// The source item's timeline ID. Only necessary for a rough Hashable conformance.
let id: TimelineItemIdentifier
/// The source item's room ID.
let roomID: String
/// The item's content to be forwarded.
let content: RoomMessageEventContentWithoutRelation
static func == (lhs: MessageForwardingItem, rhs: MessageForwardingItem) -> Bool {
lhs.id == rhs.id && lhs.roomID == rhs.roomID
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(roomID)
}
}

View File

@ -20,8 +20,10 @@ import SwiftUI
typealias MessageForwardingScreenViewModelType = StateStoreViewModel<MessageForwardingScreenViewState, MessageForwardingScreenViewAction>
class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, MessageForwardingScreenViewModelProtocol {
private let roomSummaryProvider: RoomSummaryProviderProtocol?
private let sourceRoomID: String
private let forwardingItem: MessageForwardingItem
private let clientProxy: ClientProxyProtocol
private let roomSummaryProvider: RoomSummaryProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private var actionsSubject: PassthroughSubject<MessageForwardingScreenViewModelAction, Never> = .init()
@ -29,11 +31,15 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
actionsSubject.eraseToAnyPublisher()
}
init(roomSummaryProvider: RoomSummaryProviderProtocol,
mediaProvider: MediaProviderProtocol,
sourceRoomID: String) {
init(forwardingItem: MessageForwardingItem,
clientProxy: ClientProxyProtocol,
roomSummaryProvider: RoomSummaryProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
mediaProvider: MediaProviderProtocol) {
self.forwardingItem = forwardingItem
self.clientProxy = clientProxy
self.roomSummaryProvider = roomSummaryProvider
self.sourceRoomID = sourceRoomID
self.userIndicatorController = userIndicatorController
super.init(initialViewState: MessageForwardingScreenViewState(), imageProvider: mediaProvider)
@ -49,7 +55,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
.removeDuplicates()
.sink { [weak self] searchQuery in
guard let self else { return }
self.roomSummaryProvider?.setFilter(.search(query: searchQuery))
self.roomSummaryProvider.setFilter(.search(query: searchQuery))
}
.store(in: &cancellables)
@ -60,13 +66,9 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
switch viewAction {
case .cancel:
actionsSubject.send(.dismiss)
roomSummaryProvider?.setFilter(.all(filters: []))
roomSummaryProvider.setFilter(.all(filters: []))
case .send:
guard let roomID = state.selectedRoomID else {
fatalError()
}
actionsSubject.send(.send(roomID: roomID))
Task { await forward() }
case .selectRoom(let roomID):
state.selectedRoomID = roomID
case .reachedTop:
@ -79,11 +81,6 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
// MARK: - Private
private func updateRooms() {
guard let roomSummaryProvider else {
MXLog.error("Room summary provider unavailable")
return
}
MXLog.verbose("Updating rooms")
var rooms = [MessageForwardingRoom]()
@ -93,7 +90,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
case .empty, .invalidated:
continue
case .filled(let details):
if details.id == sourceRoomID {
if details.id == forwardingItem.roomID {
continue
}
@ -114,10 +111,6 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
/// we just need the respective bounds to be there to trigger a next page load or
/// a reset to just one page
private func updateVisibleRange(edge: UIRectEdge) {
guard let roomSummaryProvider else {
return
}
switch edge {
case .top:
roomSummaryProvider.updateVisibleRange(0..<0)
@ -128,4 +121,25 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me
break
}
}
private func forward() async {
guard let roomID = state.selectedRoomID else {
fatalError()
}
guard let targetRoomProxy = await clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room to forward to with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
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
}
// Timelines are cached - the local echo will be visible when fetching the room by its ID.
actionsSubject.send(.sent(roomID: roomID))
}
}

View File

@ -104,9 +104,13 @@ private struct MessageForwardingListRow: View {
struct MessageForwardingScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))
let viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: summaryProvider,
mediaProvider: MockMediaProvider(),
sourceRoomID: "")
let viewModel = MessageForwardingScreenViewModel(forwardingItem: .init(id: .init(timelineID: ""),
roomID: "",
content: .init(noPointer: .init())),
clientProxy: ClientProxyMock(),
roomSummaryProvider: summaryProvider,
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MockMediaProvider())
NavigationStack {
MessageForwardingScreen(context: viewModel.context)

View File

@ -42,7 +42,7 @@ enum RoomScreenCoordinatorAction {
case presentLocationViewer(body: String, geoURI: GeoURI, description: String?)
case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case presentRoomMemberDetails(userID: String)
case presentMessageForwarding(itemID: TimelineItemIdentifier)
case presentMessageForwarding(forwardingItem: MessageForwardingItem)
case presentCallScreen
}
@ -110,8 +110,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
case .displayRoomMemberDetails(userID: let userID):
actionsSubject.send(.presentRoomMemberDetails(userID: userID))
case .displayMessageForwarding(let itemID):
actionsSubject.send(.presentMessageForwarding(itemID: itemID))
case .displayMessageForwarding(let forwardingItem):
actionsSubject.send(.presentMessageForwarding(forwardingItem: forwardingItem))
case .displayLocation(let body, let geoURI, let description):
actionsSubject.send(.presentLocationViewer(body: body, geoURI: geoURI, description: description))
case .composer(let action):

View File

@ -16,7 +16,6 @@
import Combine
import SwiftUI
import UIKit
import OrderedCollections
@ -31,7 +30,7 @@ enum RoomScreenViewModelAction {
case displayPollForm(mode: PollFormMode)
case displayMediaUploadPreviewScreen(url: URL)
case displayRoomMemberDetails(userID: String)
case displayMessageForwarding(itemID: TimelineItemIdentifier)
case displayMessageForwarding(forwardingItem: MessageForwardingItem)
case displayLocation(body: String, geoURI: GeoURI, description: String?)
case composer(action: RoomScreenComposerAction)
case displayCallScreen

View File

@ -429,7 +429,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
case .displayEmojiPicker(let itemID, let selectedEmojis):
actionsSubject.send(.displayEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis))
case .displayMessageForwarding(let itemID):
actionsSubject.send(.displayMessageForwarding(itemID: itemID))
Task { await self.forwardMessage(itemID: itemID) }
case .displayPollForm(let mode):
actionsSubject.send(.displayPollForm(mode: mode))
case .displayReportContent(let itemID, let senderID):
@ -738,6 +738,13 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id)
}
// MARK: - Message forwarding
private func forwardMessage(itemID: TimelineItemIdentifier) async {
guard let content = await timelineController.messageEventContent(for: itemID) else { return }
actionsSubject.send(.displayMessageForwarding(forwardingItem: .init(id: itemID, roomID: roomProxy.id, content: content)))
}
// MARK: - User Indicators
private func showFocusLoadingIndicator() {

View File

@ -18,6 +18,7 @@
import Combine
import Foundation
import MatrixRustSDK
class MockRoomTimelineController: RoomTimelineControllerProtocol {
/// An array of timeline item arrays that will be inserted in order for each back pagination request.
@ -99,6 +100,10 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func redact(_ itemID: TimelineItemIdentifier) async { }
func messageEventContent(for itemID: TimelineItemIdentifier) -> RoomMessageEventContentWithoutRelation? {
.init(noPointer: .init())
}
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo {
.init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil)
}

View File

@ -227,6 +227,14 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
}
func messageEventContent(for itemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation? {
guard let eventID = itemID.eventID else {
MXLog.warning("The item doesn't have an event ID.")
return nil
}
return await activeTimeline.messageEventContent(for: eventID)
}
// Handle this parallel to the timeline items so we're not forced
// to bundle the Rust side objects within them
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo {

View File

@ -15,8 +15,8 @@
//
import Combine
import Foundation
import UIKit
import MatrixRustSDK
import SwiftUI
enum RoomTimelineControllerCallback {
case updatedTimelineItems(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool)
@ -68,6 +68,8 @@ protocol RoomTimelineControllerProtocol {
func redact(_ itemID: TimelineItemIdentifier) async
func messageEventContent(for itemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation?
func debugInfo(for itemID: TimelineItemIdentifier) -> TimelineItemDebugInfo
func retryDecryption(for sessionID: String) async

View File

@ -21,20 +21,29 @@ import XCTest
@MainActor
class MessageForwardingScreenViewModelTests: XCTestCase {
let forwardingItem = MessageForwardingItem(id: .init(timelineID: "t1", eventID: "t1"),
roomID: "1",
content: .init(noPointer: .init()))
var viewModel: MessageForwardingScreenViewModelProtocol!
var context: MessageForwardingScreenViewModelType.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
cancellables.removeAll()
viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
mediaProvider: MockMediaProvider(),
sourceRoomID: "1")
let clientProxy = ClientProxyMock(.init())
clientProxy.roomForIdentifierClosure = { RoomProxyMock(with: .init(id: $0)) }
viewModel = MessageForwardingScreenViewModel(forwardingItem: forwardingItem,
clientProxy: clientProxy,
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))),
userIndicatorController: UserIndicatorControllerMock(),
mediaProvider: MockMediaProvider())
context = viewModel.context
}
func testInitialState() {
XCTAssertNil(context.viewState.rooms.first(where: { $0.id == "1" }), "The source room ID shouldn't be shown")
XCTAssertNil(context.viewState.rooms.first(where: { $0.id == forwardingItem.roomID }), "The source room ID shouldn't be shown")
}
func testRoomSelection() {
@ -61,7 +70,7 @@ class MessageForwardingScreenViewModelTests: XCTestCase {
viewModel.actions
.sink { action in
switch action {
case .send(let roomID):
case .sent(let roomID):
XCTAssertEqual(roomID, "2")
expectation.fulfill()
default: