Hard code the room flow to a single room proxy. (#2599)

* Don't give the room flow a split coordinator any more.
This commit is contained in:
Doug
2024-03-27 10:56:58 +00:00
committed by GitHub
parent e424a02a2b
commit 144f3a7fc4
11 changed files with 624 additions and 401 deletions
+4
View File
@@ -596,6 +596,7 @@
9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; };
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; };
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
912B88362BADD6EA00CD00F6 /* UserSessionFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912B88352BADD6EA00CD00F6 /* UserSessionFlowCoordinatorTests.swift */; };
915B4CDAF220D9AEB4047D45 /* PollInteractionHandlerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 259E5B05BDE6E20C26CF11B4 /* PollInteractionHandlerProtocol.swift */; };
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
91C6AC0E9D2B9C0C76CC6AD4 /* RoomDirectorySearchScreenScreenModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */; };
@@ -1656,6 +1657,7 @@
90A55430639712CFACA34F43 /* TextRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRoomTimelineItem.swift; sourceTree = "<group>"; };
90DFF217B3D9D0941283278C /* RoomRolesAndPermissionsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRolesAndPermissionsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
90F2F8998E5632668B0AD848 /* RoomTimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemView.swift; sourceTree = "<group>"; };
912B88352BADD6EA00CD00F6 /* UserSessionFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionFlowCoordinatorTests.swift; sourceTree = "<group>"; };
913C8E13B8B602C7B6C0C4AE /* PillTextAttachmentData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillTextAttachmentData.swift; sourceTree = "<group>"; };
91868EB98818044E6FEBE532 /* NotificationPermissionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreenCoordinator.swift; sourceTree = "<group>"; };
91CF6F7D08228D16BA69B63B /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -3535,6 +3537,7 @@
7583EAC171059A86B767209F /* MediaProvider */,
7DBC911559934065993A5FF4 /* NotificationManager */,
1C62F5382CC9D9F7DCEC344A /* UserDiscoveryService */,
912B88352BADD6EA00CD00F6 /* UserSessionFlowCoordinatorTests.swift */,
);
path = Sources;
sourceTree = "<group>";
@@ -5624,6 +5627,7 @@
50381244BA280451771BE3ED /* PINTextFieldTests.swift in Sources */,
27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */,
3982E60F9C126437D5E488A3 /* PillContextTests.swift in Sources */,
912B88362BADD6EA00CD00F6 /* UserSessionFlowCoordinatorTests.swift in Sources */,
FF7E8ECC8E7E1D1851517536 /* PollFormScreenViewModelTests.swift in Sources */,
D415764645491F10344FC6AC /* Publisher.swift in Sources */,
D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */,
@@ -54,3 +54,6 @@ protocol WindowManagerProtocol: AnyObject, OrientationManagerProtocol {
func hideGlobalSearch()
}
// sourcery: AutoMockable
extension WindowManagerProtocol { }
@@ -20,35 +20,35 @@ import SwiftState
import UserNotifications
enum RoomFlowCoordinatorAction: Equatable {
case presentedRoom(String)
case dismissedRoom
case presentRoom(roomID: String)
case presentCallScreen(roomProxy: RoomProxyProtocol)
case finished
static func == (lhs: RoomFlowCoordinatorAction, rhs: RoomFlowCoordinatorAction) -> Bool {
switch (lhs, rhs) {
case (.presentedRoom(let lhsRoomID), .presentedRoom(let rhsRoomID)):
return lhsRoomID == rhsRoomID
case (.dismissedRoom, .dismissedRoom):
return true
case (.presentRoom(let lhsRoomID), .presentRoom(let rhsRoomID)):
lhsRoomID == rhsRoomID
case (.presentCallScreen(let lhsRoomProxy), .presentCallScreen(let rhsRoomProxy)):
return lhsRoomProxy.id == rhsRoomProxy.id
lhsRoomProxy.id == rhsRoomProxy.id
case (.finished, .finished):
true
default:
return false
false
}
}
}
// swiftlint:disable file_length
class RoomFlowCoordinator: FlowCoordinatorProtocol {
private let orientationManager: OrientationManagerProtocol
private let roomProxy: RoomProxyProtocol
private let userSession: UserSessionProtocol
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let navigationStackCoordinator: NavigationStackCoordinator
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let emojiProvider: EmojiProviderProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let userIndicatorController: UserIndicatorControllerProtocol
private let orientationManager: OrientationManagerProtocol
// periphery:ignore - used to avoid deallocation
private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator?
@@ -62,34 +62,35 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
actionsSubject.eraseToAnyPublisher()
}
private var roomProxy: RoomProxyProtocol? {
didSet {
oldValue?.unsubscribeFromUpdates()
}
}
private var timelineController: RoomTimelineControllerProtocol?
init(userSession: UserSessionProtocol,
init(roomProxy: RoomProxyProtocol,
userSession: UserSessionProtocol,
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
navigationStackCoordinator: NavigationStackCoordinator,
navigationSplitCoordinator: NavigationSplitCoordinator,
emojiProvider: EmojiProviderProtocol,
appSettings: AppSettings,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol,
orientationManager: OrientationManagerProtocol) {
orientationManager: OrientationManagerProtocol) async {
self.roomProxy = roomProxy
self.userSession = userSession
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.navigationStackCoordinator = navigationStackCoordinator
self.navigationSplitCoordinator = navigationSplitCoordinator
self.emojiProvider = emojiProvider
self.appSettings = appSettings
self.analytics = analytics
self.userIndicatorController = userIndicatorController
self.orientationManager = orientationManager
// The SDK needs to handle multiple subscription calls before we can start adding child room flows.
let subscriptionTask = Task { await roomProxy.subscribeForUpdates() }
setupStateMachine()
analytics.signpost.beginRoomFlow(roomProxy.id)
_ = await subscriptionTask.result
}
// MARK: - FlowCoordinatorProtocol
@@ -101,18 +102,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
switch appRoute {
case .room(let roomID):
if case .room(let identifier) = stateMachine.state,
roomID == identifier {
return
}
stateMachine.tryEvent(.presentRoom(roomID: roomID), userInfo: EventUserInfo(animated: animated))
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
stateMachine.tryEvent(.presentRoom, userInfo: EventUserInfo(animated: animated))
case .roomDetails(let roomID):
stateMachine.tryEvent(.presentRoomDetails(roomID: roomID), userInfo: EventUserInfo(animated: animated))
guard roomID == roomProxy.id else { fatalError("Navigation route doesn't belong to this room flow.") }
stateMachine.tryEvent(.presentRoomDetails, userInfo: EventUserInfo(animated: animated))
case .roomList:
stateMachine.tryEvent(.dismissRoom, userInfo: EventUserInfo(animated: animated))
case .roomMemberDetails(let userID):
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID))
stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID), userInfo: EventUserInfo(animated: animated))
case .genericCallLink, .oidcCallback, .settings, .chatBackupSettings:
break
}
@@ -131,107 +129,105 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func setupStateMachine() {
stateMachine.addRouteMapping { event, fromState, _ in
switch (fromState, event) {
case (_, .presentRoom(let roomID)):
return .room(roomID: roomID)
case (_, .presentRoom):
return .room
case (.room, .dismissRoom):
return .initial
return .complete
case (.initial, .presentRoomDetails(let roomID)):
return .roomDetails(roomID: roomID, isRoot: true)
case (.room(let currentRoomID), .presentRoomDetails(let roomID)):
return .roomDetails(roomID: roomID, isRoot: roomID != currentRoomID)
case (.roomDetails(let currentRoomID, _), .presentRoomDetails(let roomID)):
return .roomDetails(roomID: roomID, isRoot: roomID != currentRoomID)
case (.roomDetails(let roomID, _), .dismissRoomDetails):
return .room(roomID: roomID)
case (.initial, .presentRoomDetails):
return .roomDetails(isRoot: true)
case (.room, .presentRoomDetails):
return .roomDetails(isRoot: false)
case (.roomDetails, .dismissRoomDetails):
return .room
case (.roomDetails, .dismissRoom):
return .initial
return .complete
case (.roomDetails(let roomID, _), .presentRoomDetailsEditScreen):
return .roomDetailsEditScreen(roomID: roomID)
case (.roomDetailsEditScreen(let roomID), .dismissRoomDetailsEditScreen):
return .roomDetails(roomID: roomID, isRoot: false)
case (.roomDetails, .presentRoomDetailsEditScreen):
return .roomDetailsEditScreen
case (.roomDetailsEditScreen, .dismissRoomDetailsEditScreen):
return .roomDetails(isRoot: false)
case (.roomDetails(let roomID, _), .presentNotificationSettingsScreen):
return .notificationSettings(roomID: roomID)
case (.notificationSettings(let roomID), .dismissNotificationSettingsScreen):
return .roomDetails(roomID: roomID, isRoot: false)
case (.roomDetails, .presentNotificationSettingsScreen):
return .notificationSettings
case (.notificationSettings, .dismissNotificationSettingsScreen):
return .roomDetails(isRoot: false)
case (.notificationSettings(let roomID), .presentGlobalNotificationSettingsScreen):
return .globalNotificationSettings(roomID: roomID)
case (.globalNotificationSettings(let roomID), .dismissGlobalNotificationSettingsScreen):
return .notificationSettings(roomID: roomID)
case (.notificationSettings, .presentGlobalNotificationSettingsScreen):
return .globalNotificationSettings
case (.globalNotificationSettings, .dismissGlobalNotificationSettingsScreen):
return .notificationSettings
case (.roomDetails(let roomID, _), .presentRoomMembersList):
return .roomMembersList(roomID: roomID)
case (.roomMembersList(let roomID), .dismissRoomMembersList):
return .roomDetails(roomID: roomID, isRoot: false)
case (.roomDetails, .presentRoomMembersList):
return .roomMembersList
case (.roomMembersList, .dismissRoomMembersList):
return .roomDetails(isRoot: false)
case (.room(let roomID), .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(roomID: roomID, userID: userID, fromRoomMembersList: false)
case (.roomMembersList(let roomID), .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(roomID: roomID, userID: userID, fromRoomMembersList: true)
case (.roomMemberDetails(let roomID, _, let fromRoomMembersList), .dismissRoomMemberDetails):
return fromRoomMembersList ? .roomMembersList(roomID: roomID) : .room(roomID: roomID)
case (.room, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, fromRoomMembersList: false)
case (.roomMembersList, .presentRoomMemberDetails(userID: let userID)):
return .roomMemberDetails(userID: userID, fromRoomMembersList: true)
case (.roomMemberDetails(_, let fromRoomMembersList), .dismissRoomMemberDetails):
return fromRoomMembersList ? .roomMembersList : .room
case (.roomDetails(let roomID, _), .presentInviteUsersScreen):
return .inviteUsersScreen(roomID: roomID, fromRoomMembersList: false)
case (.roomMembersList(let roomID), .presentInviteUsersScreen):
return .inviteUsersScreen(roomID: roomID, fromRoomMembersList: true)
case (.inviteUsersScreen(let roomID, let fromRoomMembersList), .dismissInviteUsersScreen):
return fromRoomMembersList ? .roomMembersList(roomID: roomID) : .roomDetails(roomID: roomID, isRoot: false)
case (.roomDetails, .presentInviteUsersScreen):
return .inviteUsersScreen(fromRoomMembersList: false)
case (.roomMembersList, .presentInviteUsersScreen):
return .inviteUsersScreen(fromRoomMembersList: true)
case (.inviteUsersScreen(let fromRoomMembersList), .dismissInviteUsersScreen):
return fromRoomMembersList ? .roomMembersList : .roomDetails(isRoot: false)
case (.room(let roomID), .presentReportContent(let itemID, let senderID)):
return .reportContent(roomID: roomID, itemID: itemID, senderID: senderID)
case (.reportContent(let roomID, _, _), .dismissReportContent):
return .room(roomID: roomID)
case (.room, .presentReportContent(let itemID, let senderID)):
return .reportContent(itemID: itemID, senderID: senderID)
case (.reportContent, .dismissReportContent):
return .room
case (.room(let roomID), .presentMediaUploadPicker(let source)):
return .mediaUploadPicker(roomID: roomID, source: source)
case (.mediaUploadPicker(let roomID, _), .dismissMediaUploadPicker):
return .room(roomID: roomID)
case (.room, .presentMediaUploadPicker(let source)):
return .mediaUploadPicker(source: source)
case (.mediaUploadPicker, .dismissMediaUploadPicker):
return .room
case (.mediaUploadPicker(let roomID, _), .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(roomID: roomID, fileURL: fileURL)
case (.room(let roomID), .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(roomID: roomID, fileURL: fileURL)
case (.mediaUploadPreview(let roomID, _), .dismissMediaUploadPreview):
return .room(roomID: roomID)
case (.mediaUploadPicker, .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(fileURL: fileURL)
case (.room, .presentMediaUploadPreview(let fileURL)):
return .mediaUploadPreview(fileURL: fileURL)
case (.mediaUploadPreview, .dismissMediaUploadPreview):
return .room
case (.room(let roomID), .presentEmojiPicker(let itemID, let selectedEmoji)):
return .emojiPicker(roomID: roomID, itemID: itemID, selectedEmojis: selectedEmoji)
case (.emojiPicker(let roomID, _, _), .dismissEmojiPicker):
return .room(roomID: roomID)
case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)):
return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji)
case (.emojiPicker, .dismissEmojiPicker):
return .room
case (.room(let roomID), .presentMessageForwarding(let itemID)):
return .messageForwarding(roomID: roomID, itemID: itemID)
case (.messageForwarding(let roomID, _), .dismissMessageForwarding):
return .room(roomID: roomID)
case (.room, .presentMessageForwarding(let itemID)):
return .messageForwarding(itemID: itemID)
case (.messageForwarding, .dismissMessageForwarding):
return .room
case (.room(let roomID), .presentMapNavigator):
return .mapNavigator(roomID: roomID)
case (.mapNavigator(let roomID), .dismissMapNavigator):
return .room(roomID: roomID)
case (.room, .presentMapNavigator):
return .mapNavigator
case (.mapNavigator, .dismissMapNavigator):
return .room
case (.room(let roomID), .presentPollForm):
return .pollForm(roomID: roomID)
case (.pollForm(let roomID), .dismissPollForm):
return .room(roomID: roomID)
case (.room, .presentPollForm):
return .pollForm
case (.pollForm, .dismissPollForm):
return .room
case (.roomDetails(let roomID, _), .presentPollsHistory):
return .pollsHistory(roomID: roomID)
case (.pollsHistory(let roomID), .dismissPollsHistory):
return .roomDetails(roomID: roomID, isRoot: false)
case (.roomDetails, .presentPollsHistory):
return .pollsHistory
case (.pollsHistory, .dismissPollsHistory):
return .roomDetails(isRoot: false)
case (.pollsHistory(let roomID), .presentPollForm):
return .pollsHistoryForm(roomID: roomID)
case (.pollsHistoryForm(let roomID), .dismissPollForm):
return .pollsHistory(roomID: roomID)
case (.pollsHistory, .presentPollForm):
return .pollsHistoryForm
case (.pollsHistoryForm, .dismissPollForm):
return .pollsHistory
case (.roomDetails(let roomID, _), .presentRolesAndPermissionsScreen):
return .rolesAndPermissions(roomID: roomID)
case (.rolesAndPermissions(let roomID), .dismissRolesAndPermissionsScreen):
return .roomDetails(roomID: roomID, isRoot: false)
case (.roomDetails, .presentRolesAndPermissionsScreen):
return .rolesAndPermissions
case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen):
return .roomDetails(isRoot: false)
default:
return nil
@@ -244,25 +240,19 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let animated = (context.userInfo as? EventUserInfo)?.animated ?? true
switch (context.fromState, context.event, context.toState) {
case (.roomDetails(roomID: let currentRoomID, true), .presentRoom(let roomID), .room) where currentRoomID == roomID:
dismissRoom(animated: animated)
presentRoom(roomID, animated: animated)
case (_, .presentRoom(let roomID), .room):
let destinationRoomProxy = (context.userInfo as? EventUserInfo)?.destinationRoomProxy
presentRoom(roomID, animated: animated, destinationRoomProxy: destinationRoomProxy)
case (.room, .dismissRoom, .initial):
dismissRoom(animated: animated)
case (_, .presentRoom, .room):
Task { await self.presentRoom(animated: animated) }
case (.room, .dismissRoom, .complete):
dismissFlow(animated: animated)
case (.roomDetails(let currentRoomID, _), .presentRoomDetails, .roomDetails(let roomID, _)) where currentRoomID == roomID:
break
case (.initial, .presentRoomDetails, .roomDetails(let roomID, let isRoot)),
(.room, .presentRoomDetails, .roomDetails(let roomID, let isRoot)),
(.roomDetails, .presentRoomDetails, .roomDetails(let roomID, let isRoot)):
self.presentRoomDetails(roomID: roomID, isRoot: isRoot, animated: animated)
case (.initial, .presentRoomDetails, .roomDetails(let isRoot)),
(.room, .presentRoomDetails, .roomDetails(let isRoot)),
(.roomDetails, .presentRoomDetails, .roomDetails(let isRoot)):
Task { await self.presentRoomDetails(isRoot: isRoot, animated: animated) }
case (.roomDetails, .dismissRoomDetails, .room):
break
case (.roomDetails, .dismissRoom, .initial):
dismissRoom(animated: animated)
case (.roomDetails, .dismissRoom, .complete):
dismissFlow(animated: animated)
case (.roomDetails, .presentRoomDetailsEditScreen, .roomDetailsEditScreen):
presentRoomDetailsEditScreen()
@@ -284,12 +274,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.roomMembersList, .dismissRoomMembersList, .roomDetails):
break
case (.room, .presentRoomMemberDetails, .roomMemberDetails(_, let userID, _)):
case (.room, .presentRoomMemberDetails, .roomMemberDetails(let userID, _)):
presentRoomMemberDetails(userID: userID)
case (.roomMemberDetails, .dismissRoomMemberDetails, .room):
break
case (.roomMembersList, .presentRoomMemberDetails, .roomMemberDetails(_, let userID, _)):
case (.roomMembersList, .presentRoomMemberDetails, .roomMemberDetails(let userID, _)):
presentRoomMemberDetails(userID: userID)
case (.roomMemberDetails, .dismissRoomMemberDetails, .roomMembersList):
break
@@ -304,24 +294,24 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.inviteUsersScreen, .dismissInviteUsersScreen, .roomMembersList):
break
case (.room, .presentReportContent, .reportContent(_, let itemID, let senderID)):
case (.room, .presentReportContent, .reportContent(let itemID, let senderID)):
presentReportContent(for: itemID, from: senderID)
case (.reportContent, .dismissReportContent, .room):
break
case (.room, .presentMediaUploadPicker, .mediaUploadPicker(_, let source)):
case (.room, .presentMediaUploadPicker, .mediaUploadPicker(let source)):
presentMediaUploadPickerWithSource(source)
case (.mediaUploadPicker, .dismissMediaUploadPicker, .room):
break
case (.mediaUploadPicker, .presentMediaUploadPreview, .mediaUploadPreview(_, let fileURL)):
case (.mediaUploadPicker, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL)):
presentMediaUploadPreviewScreen(for: fileURL)
case (.room, .presentMediaUploadPreview, .mediaUploadPreview(_, let fileURL)):
case (.room, .presentMediaUploadPreview, .mediaUploadPreview(let fileURL)):
presentMediaUploadPreviewScreen(for: fileURL)
case (.mediaUploadPreview, .dismissMediaUploadPreview, .room):
break
case (.room, .presentEmojiPicker, .emojiPicker(_, let itemID, let selectedEmoji)):
case (.room, .presentEmojiPicker, .emojiPicker(let itemID, let selectedEmoji)):
presentEmojiPicker(for: itemID, selectedEmoji: selectedEmoji)
case (.emojiPicker, .dismissEmojiPicker, .room):
break
@@ -385,42 +375,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
/// - destinationRoomProxy: an optional already build roomProxy for the target room. It is currently used when
/// forwarding messages so that we can take advantage of the local echo
/// and have the message already there when presenting the room
private func presentRoom(_ roomID: String, animated: Bool, destinationRoomProxy: RoomProxyProtocol? = nil) {
Task {
await asyncPresentRoom(roomID, animated: animated, destinationRoomProxy: destinationRoomProxy)
}
}
private func asyncPresentRoom(_ roomID: String, animated: Bool, destinationRoomProxy: RoomProxyProtocol? = nil) async {
private func presentRoom(animated: Bool) async {
// If any sheets are presented dismiss them, rely on their dismissal callbacks to transition the state machine
// through the correct states before presenting the room
navigationStackCoordinator.setSheetCoordinator(nil)
if let roomProxy, roomProxy.id == roomID {
navigationStackCoordinator.popToRoot()
return
}
let roomProxy: RoomProxyProtocol
if let destinationRoomProxy {
roomProxy = destinationRoomProxy
} else {
guard let proxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Invalid room identifier: \(roomID)")
stateMachine.tryEvent(.dismissRoom)
return
}
roomProxy = proxy
}
await roomProxy.subscribeForUpdates()
actionsSubject.send(.presentedRoom(roomID))
self.roomProxy = roomProxy
Task {
// Flag the room as read on entering, the timeline will take care of the read receipts
await roomProxy.flagAsUnread(false)
@@ -456,7 +415,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .presentRoomDetails:
stateMachine.tryEvent(.presentRoomDetails(roomID: roomID))
stateMachine.tryEvent(.presentRoomDetails)
case .presentReportContent(let itemID, let senderID):
stateMachine.tryEvent(.presentReportContent(itemID: itemID, senderID: senderID))
case .presentMediaUploadPicker(let source):
@@ -476,10 +435,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .presentMessageForwarding(let itemID):
stateMachine.tryEvent(.presentMessageForwarding(itemID: itemID))
case .presentCallScreen:
guard let roomProxy = self.roomProxy else {
fatalError()
}
actionsSubject.send(.presentCallScreen(roomProxy: roomProxy))
}
}
@@ -489,49 +444,22 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
// Move the state machine to no room selected if the room currently being dismissed
// is the same as the one selected in the state machine.
// This generally happens when popping the room screen while in a compact layout
switch self?.stateMachine.state {
case let .room(selectedRoomID) where selectedRoomID == roomID:
self?.stateMachine.tryEvent(.dismissRoom)
default:
break
}
}
if navigationSplitCoordinator.detailCoordinator == nil {
navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator, animated: animated)
self?.stateMachine.tryEvent(.dismissRoom)
}
}
private func dismissRoom(animated: Bool) {
// DON'T CHANGE THE ORDER IN WHICH POP AND SET ARE DONE, IT CAN CAUSE A CRASH
private func dismissFlow(animated: Bool) {
navigationStackCoordinator.popToRoot(animated: false)
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
roomProxy = nil
navigationStackCoordinator.setRootCoordinator(nil, animated: false)
roomProxy.unsubscribeFromUpdates()
timelineController = nil
actionsSubject.send(.dismissedRoom)
actionsSubject.send(.finished)
analytics.signpost.endRoomFlow()
}
private func presentRoomDetails(roomID: String, isRoot: Bool, animated: Bool) {
Task {
await asyncPresentRoomDetails(roomID: roomID, isRoot: isRoot, animated: animated)
}
}
private func asyncPresentRoomDetails(roomID: String, isRoot: Bool, animated: Bool) async {
if isRoot {
roomProxy = await userSession.clientProxy.roomForIdentifier(roomID)
await roomProxy?.subscribeForUpdates()
} else {
await asyncPresentRoom(roomID, animated: animated)
}
guard let roomProxy else {
MXLog.error("Invalid room identifier: \(roomID)")
stateMachine.tryEvent(.dismissRoom)
return
}
private func presentRoomDetails(isRoot: Bool, animated: Bool) async {
let params = RoomDetailsScreenCoordinatorParameters(roomProxy: roomProxy,
clientProxy: userSession.clientProxy,
mediaProvider: userSession.mediaProvider,
@@ -547,7 +475,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .leftRoom:
dismissRoom(animated: animated)
stateMachine.tryEvent(.dismissRoom)
case .presentRoomMembersList:
stateMachine.tryEvent(.presentRoomMembersList)
case .presentRoomDetailsEditScreen:
@@ -566,17 +494,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
if isRoot {
navigationStackCoordinator.setRootCoordinator(coordinator, animated: animated) { [weak self] in
guard let self else { return }
if case .roomDetails(let detailsRoomID, _) = stateMachine.state, detailsRoomID == roomID {
stateMachine.tryEvent(.dismissRoom)
}
self?.stateMachine.tryEvent(.dismissRoom) // This seems wrong but it isn't, Maybe the event should be called dismissFlow?
}
if navigationSplitCoordinator.detailCoordinator == nil {
navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator, animated: animated)
}
actionsSubject.send(.presentedRoom(roomID))
} else {
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
guard let self else { return }
@@ -588,10 +507,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentRoomMembersList() {
guard let roomProxy else {
fatalError()
}
let parameters = RoomMembersListScreenCoordinatorParameters(mediaProvider: userSession.mediaProvider,
roomProxy: roomProxy,
userIndicatorController: userIndicatorController,
@@ -618,10 +533,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentRoomDetailsEditScreen() {
guard let roomProxy else {
fatalError()
}
let stackCoordinator = NavigationStackCoordinator()
let roomDetailsEditParameters = RoomDetailsEditScreenCoordinatorParameters(roomProxy: roomProxy,
@@ -647,7 +558,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentReportContent(for itemID: TimelineItemIdentifier, from senderID: String) {
guard let roomProxy, let eventID = itemID.eventID else {
guard let eventID = itemID.eventID else {
fatalError()
}
@@ -707,10 +618,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentMediaUploadPreviewScreen(for url: URL) {
guard let roomProxy else {
fatalError()
}
let stackCoordinator = NavigationStackCoordinator()
let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController,
@@ -776,12 +683,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .selectedLocation(let geoURI, let isUserLocation):
Task {
_ = await self.roomProxy?.timeline.sendLocation(body: geoURI.bodyMessage,
geoURI: geoURI,
description: nil,
zoomLevel: 15,
assetType: isUserLocation ? .sender : .pin)
self.navigationSplitCoordinator.setSheetCoordinator(nil)
_ = await self.roomProxy.timeline.sendLocation(body: geoURI.bodyMessage,
geoURI: geoURI,
description: nil,
zoomLevel: 15,
assetType: isUserLocation ? .sender : .pin)
self.navigationStackCoordinator.setSheetCoordinator(nil)
}
self.analytics.trackComposer(inThread: false,
@@ -790,7 +697,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
messageType: isUserLocation ? .LocationUser : .LocationPin,
startsThread: nil)
case .close:
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
@@ -813,7 +720,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return
}
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.navigationStackCoordinator.setSheetCoordinator(nil)
switch action {
case .cancel:
@@ -831,18 +738,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)
navigationSplitCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissPollForm)
}
}
private func createPoll(question: String, options: [String], pollKind: Poll.Kind) {
Task {
guard let roomProxy = self.roomProxy else {
self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
let result = await roomProxy.timeline.createPoll(question: question, answers: options, pollKind: pollKind)
self.analytics.trackComposer(inThread: false,
@@ -864,11 +766,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func editPoll(pollStartID: String, question: String, options: [String], pollKind: Poll.Kind) {
Task {
guard let roomProxy = self.roomProxy else {
self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
let result = await roomProxy.timeline.editPoll(original: pollStartID, question: question, answers: options, pollKind: pollKind)
switch result {
@@ -882,7 +779,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private func deletePoll(mode: PollFormMode) {
Task {
guard case .edit(let pollStartID, _) = mode, let roomProxy = self.roomProxy else {
guard case .edit(let pollStartID, _) = mode else {
self.userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
@@ -905,10 +802,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func asyncPresentRoomPollsHistory() async {
guard let roomProxy else {
fatalError()
}
let userID = userSession.clientProxy.userID
let timelineItemFactory = RoomTimelineItemFactory(userID: userID,
@@ -940,10 +833,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentRoomMemberDetails(userID: String) {
guard let roomProxy else {
fatalError()
}
let params = RoomMemberDetailsScreenCoordinatorParameters(userID: userID,
roomProxy: roomProxy,
clientProxy: userSession.clientProxy,
@@ -968,12 +857,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
let currentDirectRoom = await userSession.clientProxy.directRoomForUserID(userID)
switch currentDirectRoom {
case .success(.some(let roomID)):
stateMachine.tryEvent(.presentRoom(roomID: roomID))
actionsSubject.send(.presentRoom(roomID: roomID))
case .success(nil):
switch await userSession.clientProxy.createDirectRoom(with: userID, expectedRoomName: displayName) {
case .success(let roomID):
analytics.trackCreatedRoom(isDM: true)
stateMachine.tryEvent(.presentRoom(roomID: roomID))
actionsSubject.send(.presentRoom(roomID: roomID))
case .failure:
userIndicatorController.alertInfo = .init(id: UUID())
}
@@ -993,7 +882,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentMessageForwarding(for itemID: TimelineItemIdentifier) {
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider, let eventID = itemID.eventID else {
guard let roomSummaryProvider = userSession.clientProxy.alternateRoomSummaryProvider, let eventID = itemID.eventID else {
fatalError()
}
@@ -1028,12 +917,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func forward(eventID: String, toRoomID roomID: String) async {
guard let roomProxy else {
MXLog.error("Failed retrieving current room with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
guard let messageEventContent = roomProxy.timeline.messageEventContent(for: eventID) else {
MXLog.error("Failed retrieving forwarded message event content for eventID: \(eventID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
@@ -1052,14 +935,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
return
}
stateMachine.tryEvent(.presentRoom(roomID: roomID), userInfo: EventUserInfo(animated: true, destinationRoomProxy: targetRoomProxy))
// This will become a child flow and we can pass the proxy down afterwards.
actionsSubject.send(.presentRoom(roomID: roomID))
}
private func presentNotificationSettingsScreen() {
guard let roomProxy else {
fatalError()
}
let parameters = RoomNotificationSettingsScreenCoordinatorParameters(notificationSettingsProxy: userSession.clientProxy.notificationSettings,
roomProxy: roomProxy,
displayAsUserDefinedRoomSettings: false)
@@ -1101,10 +982,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentInviteUsersScreen() {
guard let roomProxy else {
fatalError()
}
let selectedUsersSubject: CurrentValueSubject<[UserProfileProxy], Never> = .init([])
let stackCoordinator = NavigationStackCoordinator()
@@ -1173,8 +1050,6 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
private func presentRolesAndPermissionsScreen() {
guard let roomProxy else { fatalError() }
let parameters = RoomRolesAndPermissionsFlowCoordinatorParameters(roomProxy: roomProxy,
navigationStackCoordinator: navigationStackCoordinator,
userIndicatorController: userIndicatorController,
@@ -1208,39 +1083,41 @@ private extension RoomFlowCoordinator {
enum State: StateType {
case initial
case room(roomID: String)
case roomDetails(roomID: String, isRoot: Bool)
case roomDetailsEditScreen(roomID: String)
case notificationSettings(roomID: String)
case globalNotificationSettings(roomID: String)
case roomMembersList(roomID: String)
case roomMemberDetails(roomID: String, userID: String, fromRoomMembersList: Bool)
case inviteUsersScreen(roomID: String, fromRoomMembersList: Bool)
case mediaUploadPicker(roomID: String, source: MediaPickerScreenSource)
case mediaUploadPreview(roomID: String, fileURL: URL)
case emojiPicker(roomID: String, itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case mapNavigator(roomID: String)
case messageForwarding(roomID: String, itemID: TimelineItemIdentifier)
case reportContent(roomID: String, itemID: TimelineItemIdentifier, senderID: String)
case pollForm(roomID: String)
case pollsHistory(roomID: String)
case pollsHistoryForm(roomID: String)
case rolesAndPermissions(roomID: String)
case room
case roomDetails(isRoot: Bool)
case roomDetailsEditScreen
case notificationSettings
case globalNotificationSettings
case roomMembersList
case roomMemberDetails(userID: String, fromRoomMembersList: Bool)
case inviteUsersScreen(fromRoomMembersList: Bool)
case mediaUploadPicker(source: MediaPickerScreenSource)
case mediaUploadPreview(fileURL: URL)
case emojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set<String>)
case mapNavigator
case messageForwarding(itemID: TimelineItemIdentifier)
case reportContent(itemID: TimelineItemIdentifier, senderID: String)
case pollForm
case pollsHistory
case pollsHistoryForm
case rolesAndPermissions
/// The flow is complete and is handing control of the stack back to its parent.
case complete
}
struct EventUserInfo {
let animated: Bool
var destinationRoomProxy: RoomProxyProtocol?
}
enum Event: EventType {
case presentRoom(roomID: String)
case presentRoom
case dismissRoom
case presentReportContent(itemID: TimelineItemIdentifier, senderID: String)
case dismissReportContent
case presentRoomDetails(roomID: String)
case presentRoomDetails
case dismissRoomDetails
case presentRoomDetailsEditScreen
@@ -31,10 +31,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
private let windowManager: WindowManagerProtocol
private let bugReportService: BugReportServiceProtocol
private let appSettings: AppSettings
private let analytics: AnalyticsService
private let stateMachine: UserSessionFlowCoordinatorStateMachine
private let roomFlowCoordinator: RoomFlowCoordinator
// periphery:ignore - retaining purpose
private var roomFlowCoordinator: RoomFlowCoordinator?
private let roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol
private let settingsFlowCoordinator: SettingsFlowCoordinator
@@ -58,6 +61,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
actionsSubject.eraseToAnyPublisher()
}
/// For testing purposes.
var statePublisher: AnyPublisher<UserSessionFlowCoordinatorStateMachine.State, Never> { stateMachine.statePublisher }
init(userSession: UserSessionProtocol,
navigationRootCoordinator: NavigationRootCoordinator,
windowManager: WindowManagerProtocol,
@@ -73,7 +79,9 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self.navigationRootCoordinator = navigationRootCoordinator
self.windowManager = windowManager
self.bugReportService = bugReportService
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.appSettings = appSettings
self.analytics = analytics
navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: PlaceholderScreenCoordinator())
@@ -81,16 +89,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
detailNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator)
navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator)
roomFlowCoordinator = RoomFlowCoordinator(userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator,
navigationSplitCoordinator: navigationSplitCoordinator,
emojiProvider: EmojiProvider(),
appSettings: appSettings,
analytics: analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
orientationManager: windowManager)
settingsFlowCoordinator = SettingsFlowCoordinator(parameters: .init(userSession: userSession,
windowManager: windowManager,
@@ -125,28 +123,6 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
.store(in: &cancellables)
roomFlowCoordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .presentedRoom(let roomID):
analytics.signpost.beginRoomFlow(roomID)
let availableInvitesCount = userSession.clientProxy.inviteSummaryProvider?.roomListPublisher.value.count ?? 0
if case .invitesScreen = stateMachine.state, availableInvitesCount == 1 {
dismissInvitesList(animated: true)
}
stateMachine.processEvent(.selectRoom(roomID: roomID))
case .dismissedRoom:
stateMachine.processEvent(.deselectRoom)
analytics.signpost.endRoomFlow()
case .presentCallScreen(let roomProxy):
presentCallScreen(roomProxy: roomProxy)
}
}
.store(in: &cancellables)
settingsFlowCoordinator.actions.sink { [weak self] action in
guard let self else { return }
@@ -202,11 +178,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
switch appRoute {
case .room(let roomID):
Task {
await self.handleRoomRoute(roomID: roomID, animated: animated)
Task { await self.handleRoomRoute(roomID: roomID, animated: animated) }
case .roomDetails(let roomID):
if stateMachine.state.selectedRoomID == roomID {
roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
} else {
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true), userInfo: .init(animated: animated))
}
case .roomDetails, .roomList, .roomMemberDetails:
self.roomFlowCoordinator.handleAppRoute(appRoute, animated: animated)
case .roomList, .roomMemberDetails:
self.roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated)
case .genericCallLink(let url):
self.navigationSplitCoordinator.setSheetCoordinator(GenericCallLinkCoordinator(parameters: .init(url: url)), animated: animated)
case .oidcCallback:
@@ -221,11 +201,11 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
switch await userSession.clientProxy.roomForIdentifier(roomID)?.membership {
case .invited:
if UIDevice.current.isPhone {
roomFlowCoordinator.clearRoute(animated: animated)
roomFlowCoordinator?.clearRoute(animated: animated)
}
stateMachine.processEvent(.showInvitesScreen, userInfo: .init(animated: animated))
case .joined:
roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: animated)
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false), userInfo: .init(animated: animated))
case .left, .none:
// Do nothing but maybe we should ask design to have some kind of error state
break
@@ -233,7 +213,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
}
func clearRoute(animated: Bool) {
roomFlowCoordinator.clearRoute(animated: animated)
roomFlowCoordinator?.handleAppRoute(.roomList, animated: animated)
}
// MARK: - Private
@@ -268,15 +248,15 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentHomeScreen()
attemptStartingOnboarding()
case(.roomList, .selectRoom, .roomList):
break
case(.roomList(let currentRoomID), .selectRoom(let roomID, let showingRoomDetails), .roomList):
Task { await self.presentRoomFlow(roomID: roomID, showingRoomDetails: showingRoomDetails, animated: animated) }
case(.roomList, .deselectRoom, .roomList):
break
tearDownRoomFlow(animated: animated)
case (.invitesScreen, .selectRoom, .invitesScreen):
break
case (.invitesScreen, .deselectRoom, .invitesScreen):
break
tearDownRoomFlow(animated: animated)
case (.roomList, .showSettingsScreen, .settingsScreen):
break
@@ -353,13 +333,13 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
switch action {
case .presentRoom(let roomID):
roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true)
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
case .presentRoomDetails(let roomID):
roomFlowCoordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true)
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: true))
case .roomLeft(let roomID):
if case .roomList(selectedRoomID: let selectedRoomID) = stateMachine.state,
selectedRoomID == roomID {
roomFlowCoordinator.handleAppRoute(.roomList, animated: true)
clearRoute(animated: true)
}
case .presentSettingsScreen:
settingsFlowCoordinator.handleAppRoute(.settings, animated: true)
@@ -431,6 +411,62 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
presentSecureBackupLogoutConfirmationScreen()
}
// MARK: Room Flow
private func presentRoomFlow(roomID: String, showingRoomDetails: Bool, animated: Bool) async {
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Invalid room ID: \(roomID)")
return
}
let coordinator = await RoomFlowCoordinator(roomProxy: roomProxy,
userSession: userSession,
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator,
emojiProvider: EmojiProvider(),
appSettings: appSettings,
analytics: analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
orientationManager: windowManager)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .finished:
stateMachine.processEvent(.deselectRoom)
case .presentRoom(let roomID):
stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
case .presentCallScreen(let roomProxy):
presentCallScreen(roomProxy: roomProxy)
}
}
.store(in: &cancellables)
roomFlowCoordinator = coordinator
if navigationSplitCoordinator.detailCoordinator !== detailNavigationStackCoordinator {
navigationSplitCoordinator.setDetailCoordinator(detailNavigationStackCoordinator, animated: animated)
}
if showingRoomDetails {
coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated)
} else {
coordinator.handleAppRoute(.room(roomID: roomID), animated: animated)
}
let availableInvitesCount = userSession.clientProxy.inviteSummaryProvider?.roomListPublisher.value.count ?? 0
if case .invitesScreen = stateMachine.state, availableInvitesCount == 1 {
dismissInvitesList(animated: true)
}
}
private func tearDownRoomFlow(animated: Bool) {
// THIS MUST BE CALLED *AFTER* THE FLOW HAS TIDIED UP THE STACK OR IT CAN CAUSE A CRASH.
navigationSplitCoordinator.setDetailCoordinator(nil, animated: animated)
roomFlowCoordinator = nil
}
// MARK: Start Chat
private func presentStartChat(animated: Bool) {
@@ -451,7 +487,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
self.navigationSplitCoordinator.setSheetCoordinator(nil)
case .openRoom(let roomID):
self.navigationSplitCoordinator.setSheetCoordinator(nil)
self.roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true)
self.stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
}
}
.store(in: &cancellables)
@@ -473,7 +509,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
.sink { [weak self] action in
switch action {
case .openRoom(let roomID):
self?.roomFlowCoordinator.handleAppRoute(.room(roomID: roomID), animated: true)
self?.stateMachine.processEvent(.selectRoom(roomID: roomID, showingRoomDetails: false))
}
}
.store(in: &cancellables)
@@ -14,6 +14,7 @@
// limitations under the License.
//
import Combine
import Foundation
import SwiftState
@@ -38,11 +39,27 @@ class UserSessionFlowCoordinatorStateMachine {
/// Showing invites list screen
case invitesScreen(selectedRoomID: String?)
// Showing the logout flows
/// Showing the logout flows
case logoutConfirmationScreen(selectedRoomID: String?)
// Showing Room Directory Search screen
/// Showing Room Directory Search screen
case roomDirectorySearchScreen(selectedRoomID: String?)
/// The selected room ID from the state if available.
var selectedRoomID: String? {
switch self {
case .initial:
nil
case .roomList(let selectedRoomID),
.feedbackScreen(let selectedRoomID),
.settingsScreen(let selectedRoomID),
.startChatScreen(let selectedRoomID),
.invitesScreen(let selectedRoomID),
.logoutConfirmationScreen(let selectedRoomID),
.roomDirectorySearchScreen(let selectedRoomID):
selectedRoomID
}
}
}
struct EventUserInfo {
@@ -56,7 +73,7 @@ class UserSessionFlowCoordinatorStateMachine {
/// Request presentation for a particular room
/// - Parameter roomID:the room identifier
case selectRoom(roomID: String)
case selectRoom(roomID: String, showingRoomDetails: Bool)
/// The room screen has been dismissed
case deselectRoom
@@ -96,6 +113,11 @@ class UserSessionFlowCoordinatorStateMachine {
stateMachine.state
}
var stateSubject = PassthroughSubject<State, Never>()
var statePublisher: AnyPublisher<State, Never> {
stateSubject.eraseToAnyPublisher()
}
init() {
stateMachine = StateMachine(state: .initial)
configure()
@@ -106,9 +128,9 @@ class UserSessionFlowCoordinatorStateMachine {
stateMachine.addRouteMapping { event, fromState, _ in
switch (fromState, event) {
case (.roomList, .selectRoom(let roomID)):
case (.roomList, .selectRoom(let roomID, _)):
return .roomList(selectedRoomID: roomID)
case (.invitesScreen, .selectRoom(let roomID)):
case (.invitesScreen, .selectRoom(let roomID, _)):
return .invitesScreen(selectedRoomID: roomID)
case (.roomList, .deselectRoom):
return .roomList(selectedRoomID: nil)
@@ -160,6 +182,10 @@ class UserSessionFlowCoordinatorStateMachine {
MXLog.info("Transitioning from \(context.fromState)` to `\(context.toState)`")
}
}
addTransitionHandler { [weak self] context in
self?.stateSubject.send(context.toState)
}
}
/// Attempt to move the state machine to another state through an event
+3 -1
View File
@@ -84,12 +84,14 @@ extension ClientProxyMock {
guard let room = self?.roomSummaryProvider?.roomListPublisher.value.first(where: { $0.id == identifier }) else {
return nil
}
let roomID = room.id ?? UUID().uuidString
switch room {
case .empty:
return await RoomProxyMock(with: .init(name: "Empty room"))
case .filled(let details), .invalidated(let details):
return await RoomProxyMock(with: .init(name: details.name))
return await RoomProxyMock(with: .init(id: roomID, name: details.name))
}
}
}
@@ -4351,4 +4351,109 @@ class VoiceMessageRecorderMock: VoiceMessageRecorderProtocol {
}
}
}
class WindowManagerMock: WindowManagerProtocol {
weak var delegate: WindowManagerDelegate?
var mainWindow: UIWindow!
var overlayWindow: UIWindow!
var globalSearchWindow: UIWindow!
var alternateWindow: UIWindow!
var windows: [UIWindow] = []
//MARK: - configure
var configureWithCallsCount = 0
var configureWithCalled: Bool {
return configureWithCallsCount > 0
}
var configureWithReceivedWindowScene: UIWindowScene?
var configureWithReceivedInvocations: [UIWindowScene] = []
var configureWithClosure: ((UIWindowScene) -> Void)?
func configure(with windowScene: UIWindowScene) {
configureWithCallsCount += 1
configureWithReceivedWindowScene = windowScene
configureWithReceivedInvocations.append(windowScene)
configureWithClosure?(windowScene)
}
//MARK: - switchToMain
var switchToMainCallsCount = 0
var switchToMainCalled: Bool {
return switchToMainCallsCount > 0
}
var switchToMainClosure: (() -> Void)?
func switchToMain() {
switchToMainCallsCount += 1
switchToMainClosure?()
}
//MARK: - switchToAlternate
var switchToAlternateCallsCount = 0
var switchToAlternateCalled: Bool {
return switchToAlternateCallsCount > 0
}
var switchToAlternateClosure: (() -> Void)?
func switchToAlternate() {
switchToAlternateCallsCount += 1
switchToAlternateClosure?()
}
//MARK: - showGlobalSearch
var showGlobalSearchCallsCount = 0
var showGlobalSearchCalled: Bool {
return showGlobalSearchCallsCount > 0
}
var showGlobalSearchClosure: (() -> Void)?
func showGlobalSearch() {
showGlobalSearchCallsCount += 1
showGlobalSearchClosure?()
}
//MARK: - hideGlobalSearch
var hideGlobalSearchCallsCount = 0
var hideGlobalSearchCalled: Bool {
return hideGlobalSearchCallsCount > 0
}
var hideGlobalSearchClosure: (() -> Void)?
func hideGlobalSearch() {
hideGlobalSearchCallsCount += 1
hideGlobalSearchClosure?()
}
//MARK: - setOrientation
var setOrientationCallsCount = 0
var setOrientationCalled: Bool {
return setOrientationCallsCount > 0
}
var setOrientationReceivedOrientation: UIInterfaceOrientationMask?
var setOrientationReceivedInvocations: [UIInterfaceOrientationMask] = []
var setOrientationClosure: ((UIInterfaceOrientationMask) -> Void)?
func setOrientation(_ orientation: UIInterfaceOrientationMask) {
setOrientationCallsCount += 1
setOrientationReceivedOrientation = orientation
setOrientationReceivedInvocations.append(orientation)
setOrientationClosure?(orientation)
}
//MARK: - lockOrientation
var lockOrientationCallsCount = 0
var lockOrientationCalled: Bool {
return lockOrientationCallsCount > 0
}
var lockOrientationReceivedOrientation: UIInterfaceOrientationMask?
var lockOrientationReceivedInvocations: [UIInterfaceOrientationMask] = []
var lockOrientationClosure: ((UIInterfaceOrientationMask) -> Void)?
func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
lockOrientationCallsCount += 1
lockOrientationReceivedOrientation = orientation
lockOrientationReceivedInvocations.append(orientation)
lockOrientationClosure?(orientation)
}
}
// swiftlint:enable all
@@ -68,6 +68,7 @@ extension RoomProxyMock {
timeline = configuration.timeline
ownUserID = configuration.ownUserID
membership = .joined
membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher()
typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher()
@@ -83,6 +83,34 @@ extension XCTestCase {
return deferred
}
/// XCTest utility that assists in subscribing to a publisher and deferring the failure for a particular value until some other actions have been performed.
/// - Parameters:
/// - publisher: The publisher to wait on.
/// - timeout: A timeout after which we give up.
/// - message: An optional custom expectation message
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions. The publisher's result is not returned from this fulfilment.
func deferFailure<P: Publisher>(_ publisher: P,
timeout: TimeInterval,
message: String? = nil,
until condition: @escaping (P.Output) -> Bool) -> DeferredFulfillment<Void> where P.Failure == Never {
let expectation = expectation(description: message ?? "Awaiting publisher")
expectation.isInverted = true
var hasFulfilled = false
let cancellable = publisher
.sink { value in
if condition(value), !hasFulfilled {
expectation.fulfill()
hasFulfilled = true
}
}
return DeferredFulfillment<Void> {
await self.fulfillment(of: [expectation], timeout: timeout)
cancellable.cancel()
}
}
struct DeferredFulfillment<T> {
let closure: () async throws -> T
@discardableResult func fulfill() async throws -> T {
@@ -38,86 +38,63 @@ class RoomFlowCoordinatorTests: XCTestCase {
navigationStackCoordinator = NavigationStackCoordinator()
navigationSplitCoordinator.setDetailCoordinator(navigationStackCoordinator)
roomFlowCoordinator = RoomFlowCoordinator(userSession: userSession,
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
navigationStackCoordinator: navigationStackCoordinator,
navigationSplitCoordinator: navigationSplitCoordinator,
emojiProvider: EmojiProvider(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
orientationManager: OrientationManagerMock())
roomFlowCoordinator = await RoomFlowCoordinator(roomProxy: RoomProxyMock(with: .init(id: "1")),
userSession: userSession,
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
navigationStackCoordinator: navigationStackCoordinator,
emojiProvider: EmojiProvider(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: ServiceLocator.shared.userIndicatorController,
orientationManager: OrientationManagerMock())
}
func testRoomPresentation() async throws {
try await process(route: .room(roomID: "1"), expectedAction: .presentedRoom("1"))
try await process(route: .room(roomID: "1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
try await process(route: .roomList, expectedAction: .dismissedRoom)
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
try await process(route: .room(roomID: "1"), expectedAction: .presentedRoom("1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
try await process(route: .room(roomID: "2"), expectedAction: .presentedRoom("2"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
try await process(route: .roomList, expectedAction: .dismissedRoom)
try await process(route: .roomList, expectedAction: .finished)
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
}
func testRoomDetailsPresentation() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedAction: .presentedRoom("1"))
try await process(route: .roomDetails(roomID: "1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
try await process(route: .roomList, expectedAction: .dismissedRoom)
try await process(route: .roomList, expectedAction: .finished)
XCTAssertNil(navigationStackCoordinator.rootCoordinator)
}
func testStackUnwinding() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedAction: .presentedRoom("1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
try await process(route: .room(roomID: "2"), expectedAction: .presentedRoom("2"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
}
func testNoOp() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedAction: .presentedRoom("1"))
try await process(route: .roomDetails(roomID: "1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
let detailsCoordinator = navigationStackCoordinator.rootCoordinator
roomFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
await Task.yield()
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
}
func testSwitchToDifferentDetails() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedAction: .presentedRoom("1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
try await process(route: .roomDetails(roomID: "2"), expectedAction: .presentedRoom("2"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssert(navigationStackCoordinator.rootCoordinator === detailsCoordinator)
}
func testPushDetails() async throws {
try await process(route: .room(roomID: "1"), expectedAction: .presentedRoom("1"))
try await process(route: .room(roomID: "1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 0)
try await process(route: .roomDetails(roomID: "1"), expectedAction: .presentedRoom("1"))
try await process(route: .roomDetails(roomID: "1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(navigationStackCoordinator.stackCoordinators.count, 1)
XCTAssert(navigationStackCoordinator.stackCoordinators.first is RoomDetailsScreenCoordinator)
}
func testReplaceDetailsWithTimeline() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedAction: .presentedRoom("1"))
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomDetailsScreenCoordinator)
try await process(route: .room(roomID: "1"), expectedActions: [.dismissedRoom, .presentedRoom("1")])
XCTAssert(navigationStackCoordinator.rootCoordinator is RoomScreenCoordinator)
}
// MARK: - Private
private func process(route: AppRoute) async throws {
roomFlowCoordinator.handleAppRoute(route, animated: true)
await Task.yield()
}
private func process(route: AppRoute, expectedAction: RoomFlowCoordinatorAction) async throws {
try await process(route: route, expectedActions: [expectedAction])
}
@@ -0,0 +1,164 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import XCTest
import Combine
@testable import ElementX
@MainActor
class UserSessionFlowCoordinatorTests: XCTestCase {
var userSessionFlowCoordinator: UserSessionFlowCoordinator!
var navigationRootCoordinator: NavigationRootCoordinator!
var cancellables = Set<AnyCancellable>()
var detailCoordinator: CoordinatorProtocol? {
let navigationSplitCoordinator = navigationRootCoordinator.rootCoordinator as? NavigationSplitCoordinator
return navigationSplitCoordinator?.detailCoordinator
}
var detailNavigationStack: NavigationStackCoordinator? {
detailCoordinator as? NavigationStackCoordinator
}
override func setUp() async throws {
cancellables.removeAll()
let clientProxy = ClientProxyMock(.init(userID: "hi@bob", roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded(.mockRooms)))))
let mediaProvider = MockMediaProvider()
let voiceMessageMediaManager = VoiceMessageMediaManagerMock()
let userSession = MockUserSession(clientProxy: clientProxy,
mediaProvider: mediaProvider,
voiceMessageMediaManager: voiceMessageMediaManager)
navigationRootCoordinator = NavigationRootCoordinator()
userSessionFlowCoordinator = UserSessionFlowCoordinator(userSession: userSession,
navigationRootCoordinator: navigationRootCoordinator,
windowManager: WindowManagerMock(),
appLockService: AppLockServiceMock(),
bugReportService: BugReportServiceMock(),
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
appSettings: ServiceLocator.shared.settings,
analytics: ServiceLocator.shared.analytics,
notificationManager: NotificationManagerMock(),
isNewLogin: false)
let deferred = deferFulfillment(userSessionFlowCoordinator.statePublisher) { $0 == .roomList(selectedRoomID: nil) }
userSessionFlowCoordinator.start()
try await deferred.fulfill()
}
func testRoomPresentation() async throws {
try await process(route: .room(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil))
XCTAssertNil(detailNavigationStack?.rootCoordinator)
XCTAssertNil(detailCoordinator)
try await process(route: .room(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .room(roomID: "2"), expectedState: .roomList(selectedRoomID: "2"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil))
XCTAssertNil(detailNavigationStack?.rootCoordinator)
XCTAssertNil(detailCoordinator)
}
func testRoomDetailsPresentation() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .roomList, expectedState: .roomList(selectedRoomID: nil))
XCTAssertNil(detailNavigationStack?.rootCoordinator)
XCTAssertNil(detailCoordinator)
}
func testStackUnwinding() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .room(roomID: "2"), expectedState: .roomList(selectedRoomID: "2"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
}
func testNoOp() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
let unexpectedFulfillment = deferFailure(userSessionFlowCoordinator.statePublisher, timeout: 1) { _ in true }
userSessionFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
try await unexpectedFulfillment.fulfill()
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
}
func testSwitchToDifferentDetails() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .roomDetails(roomID: "2"), expectedState: .roomList(selectedRoomID: "2"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
}
func testPushDetails() async throws {
try await process(route: .room(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
let unexpectedFulfillment = deferFailure(userSessionFlowCoordinator.statePublisher, timeout: 1) { _ in true }
userSessionFlowCoordinator.handleAppRoute(.roomDetails(roomID: "1"), animated: true)
try await unexpectedFulfillment.fulfill()
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertEqual(detailNavigationStack?.stackCoordinators.count, 1)
XCTAssertTrue(detailNavigationStack?.stackCoordinators.first is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
}
func testReplaceDetailsWithTimeline() async throws {
try await process(route: .roomDetails(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomDetailsScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
try await process(route: .room(roomID: "1"), expectedState: .roomList(selectedRoomID: "1"))
XCTAssertTrue(detailNavigationStack?.rootCoordinator is RoomScreenCoordinator)
XCTAssertNotNil(detailCoordinator)
}
// MARK: - Private
private func process(route: AppRoute, expectedState: UserSessionFlowCoordinatorStateMachine.State) async throws {
// Sometimes the state machine's state changes before the coordinators have updated the stack.
let delayedPublisher = userSessionFlowCoordinator.statePublisher.delay(for: .milliseconds(10), scheduler: DispatchQueue.main)
let deferred = deferFulfillment(delayedPublisher) { $0 == expectedState }
userSessionFlowCoordinator.handleAppRoute(route, animated: true)
try await deferred.fulfill()
}
}