mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user