mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-10-31 08:15:24 +00:00
Introduce a user session manager (#6295)
This commit is contained in:
@@ -174,6 +174,7 @@
|
||||
18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F104596B0620CEFE5DFD31B1 /* RoomSelectionScreenCoordinator.swift */; };
|
||||
192A3CDCD0174AD1E4A128E4 /* AudioRecorderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2441E2424E78A40FC95DBA76 /* AudioRecorderTests.swift */; };
|
||||
194585F6CD77242B36D4ADF1 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADECBBB672497BCD4822468 /* Result.swift */; };
|
||||
1948348595A636C3B2C36836 /* UserSessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278B47B68F3079389DD34D35 /* UserSessionManagerTests.swift */; };
|
||||
1950A80CD198BED283DFC2CE /* ClientProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18F2958E6D247AE2516BEEE8 /* ClientProxy.swift */; };
|
||||
197441F1EF23A5DABACCA79F /* StickerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5338450E6783A576B5C16DD /* StickerRoomTimelineView.swift */; };
|
||||
19946B8EEE158356EB5E1107 /* LocationShareSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9406BD5F99685C5ABDEDD93 /* LocationShareSheet.swift */; };
|
||||
@@ -619,6 +620,7 @@
|
||||
6298AB0906DDD3525CD78C6B /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 0DD568A494247444A4B56031 /* Kingfisher */; };
|
||||
62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; };
|
||||
62C5876C4254C58C2086F0DE /* HomeScreenContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */; };
|
||||
63129D147B94BAD61A2C1CA1 /* UserSessionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6AEB10558A35EC5D86FF1F5 /* UserSessionDetails.swift */; };
|
||||
633400018E07D2DC7175B16E /* LiveLocationShareProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA12A7F5EF5C6D0B992869ED /* LiveLocationShareProxy.swift */; };
|
||||
633501761094E09DFBEBFFAD /* CopyTextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */; };
|
||||
6386EA3C898AD1A4BC1DC8A5 /* TimelineMediaPreviewModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD40B92FCF20165658296AD /* TimelineMediaPreviewModifier.swift */; };
|
||||
@@ -873,6 +875,7 @@
|
||||
877D3CE8680536DB430DE6A2 /* TimelineItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E48C91C8BE55CAE1A3DBC3BC /* TimelineItemIdentifier.swift */; };
|
||||
878070573C7BF19E735707B4 /* RoomTimelineItemProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */; };
|
||||
879013B1A7DA779C7E0C56A4 /* TrackedUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 004397AC54CAE108B6E2385D /* TrackedUserDefaults.swift */; };
|
||||
87DCBD9BC3A00D3E5849460B /* UserSessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C309DD8B55DA72E69CB652FC /* UserSessionManager.swift */; };
|
||||
87E8C31FCF9276F15CB0B408 /* ThreadTimelineScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C1C19A4BE46EDE1411ECCE /* ThreadTimelineScreenViewModelProtocol.swift */; };
|
||||
8810A2A30A68252EBB54EE05 /* HomeScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71BC7CA1BC1041E93077BBA1 /* HomeScreenModels.swift */; };
|
||||
88272A714D81BC507F76A1EE /* RageshakeConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FC598338E7CF41107293AB5 /* RageshakeConfiguration.swift */; };
|
||||
@@ -1036,6 +1039,7 @@
|
||||
9FBE1FB20171012260A32492 /* TimelineSenderAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D53FCCE44F96E0BC411A6CF0 /* TimelineSenderAvatarView.swift */; };
|
||||
9FBF0078657CA4F2A9747E98 /* SpaceRoomListProxyMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB2A5FF1E68BA580A20D405 /* SpaceRoomListProxyMock.swift */; };
|
||||
9FC79DA30AE0E1502DAEBD51 /* StartChatFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477A8C656B7B26E99C35927F /* StartChatFlowCoordinator.swift */; };
|
||||
9FCE652911C1CDA6DC51924B /* UserSessionManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED58B554B4D31EC752C0E6DB /* UserSessionManagerProtocol.swift */; };
|
||||
A009BDFB0A6816D4C392ADCB /* SettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF715D4FD4710EBB637D661 /* SettingsScreenViewModelProtocol.swift */; };
|
||||
A021827B528F1EDC9101CA58 /* AppCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBC776F301D374A3298C69DA /* AppCoordinatorProtocol.swift */; };
|
||||
A0601810597769B81C2358AF /* EncryptionResetPasswordScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */; };
|
||||
@@ -2035,6 +2039,7 @@
|
||||
2734CCFE59E5EDC216602910 /* EncryptionSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionSDKMock.swift; sourceTree = "<group>"; };
|
||||
2757B1BE23DF8AA239937243 /* AudioConverterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioConverterProtocol.swift; sourceTree = "<group>"; };
|
||||
276833816F5DEB1CE3B8BE1E /* ReportRoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreen.swift; sourceTree = "<group>"; };
|
||||
278B47B68F3079389DD34D35 /* UserSessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionManagerTests.swift; sourceTree = "<group>"; };
|
||||
27A1AD6389A4659AF0CEAE62 /* NotificationServiceExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtension.swift; sourceTree = "<group>"; };
|
||||
27A5FBB74981BF8EFFDAE90D /* HTMLFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFixtures.swift; sourceTree = "<group>"; };
|
||||
27A6C38B29A60E187D6A4B0A /* RoomSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSDKMock.swift; sourceTree = "<group>"; };
|
||||
@@ -2962,6 +2967,7 @@
|
||||
C2886615BEBAE33A0AA4D5F8 /* RoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenModels.swift; sourceTree = "<group>"; };
|
||||
C2B8AA3199BAAD066E36CB07 /* RoomListSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListSDKMock.swift; sourceTree = "<group>"; };
|
||||
C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreen.swift; sourceTree = "<group>"; };
|
||||
C309DD8B55DA72E69CB652FC /* UserSessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionManager.swift; sourceTree = "<group>"; };
|
||||
C30F45308428A4D9FFDB2FB8 /* BannedRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannedRoomProxy.swift; sourceTree = "<group>"; };
|
||||
C3141EE5B2CBEC45270E01EB /* CreateRoomSpaceSelectionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomSpaceSelectionSheet.swift; sourceTree = "<group>"; };
|
||||
C3285BD95B564CA2A948E511 /* OnboardingFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||
@@ -3223,6 +3229,7 @@
|
||||
ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
|
||||
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
ED58B554B4D31EC752C0E6DB /* UserSessionManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionManagerProtocol.swift; sourceTree = "<group>"; };
|
||||
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
|
||||
EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||
EDE997F400545F3265935B53 /* UserSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsTests.swift; sourceTree = "<group>"; };
|
||||
@@ -3283,6 +3290,7 @@
|
||||
F5E23D8EE6CBACF32F1EC874 /* MediaPlayerProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayerProviderProtocol.swift; sourceTree = "<group>"; };
|
||||
F5F7E6F5D5A54C68DC2EDD71 /* UserSessionStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreTests.swift; sourceTree = "<group>"; };
|
||||
F64A8582F65567AC38C2976A /* PollFormScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
F6AEB10558A35EC5D86FF1F5 /* UserSessionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionDetails.swift; sourceTree = "<group>"; };
|
||||
F70373EF23A73181D2568D79 /* SyncServiceBuilderSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncServiceBuilderSDKMock.swift; sourceTree = "<group>"; };
|
||||
F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = "<group>"; };
|
||||
F78B4E56DBFFD4A7A39D10F5 /* AudioPlaybackSpeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlaybackSpeed.swift; sourceTree = "<group>"; };
|
||||
@@ -5429,6 +5437,7 @@
|
||||
BA241DEEF7C8A7181C0AEDC9 /* UserPreferenceTests.swift */,
|
||||
71E2E5103702D13361D09100 /* UserProfileScreenViewModelTests.swift */,
|
||||
4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */,
|
||||
278B47B68F3079389DD34D35 /* UserSessionManagerTests.swift */,
|
||||
F5F7E6F5D5A54C68DC2EDD71 /* UserSessionStoreTests.swift */,
|
||||
EDE997F400545F3265935B53 /* UserSettingsTests.swift */,
|
||||
283974987DA7EC61D2AB57D9 /* VoiceMessageCacheTests.swift */,
|
||||
@@ -6885,6 +6894,9 @@
|
||||
children = (
|
||||
3558A15CFB934F9229301527 /* RestorationToken.swift */,
|
||||
43C2067FF58B4996323EB40C /* SessionDirectories.swift */,
|
||||
F6AEB10558A35EC5D86FF1F5 /* UserSessionDetails.swift */,
|
||||
C309DD8B55DA72E69CB652FC /* UserSessionManager.swift */,
|
||||
ED58B554B4D31EC752C0E6DB /* UserSessionManagerProtocol.swift */,
|
||||
0E8BDC092D817B68CD9040C5 /* UserSessionStore.swift */,
|
||||
BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */,
|
||||
);
|
||||
@@ -8588,6 +8600,7 @@
|
||||
04F17DE71A50206336749BAC /* UserPreferenceTests.swift in Sources */,
|
||||
73F547BEB41D3DAFAAF6E0AF /* UserProfileScreenViewModelTests.swift in Sources */,
|
||||
627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */,
|
||||
1948348595A636C3B2C36836 /* UserSessionManagerTests.swift in Sources */,
|
||||
E3E1B656F2E8F2D8075F6558 /* UserSessionStoreTests.swift in Sources */,
|
||||
51DBB7D725080FDAA4AB1412 /* UserSettingsTests.swift in Sources */,
|
||||
21AFEFB8CEFE56A3811A1F5B /* VoiceMessageCacheTests.swift in Sources */,
|
||||
@@ -9803,8 +9816,11 @@
|
||||
46A183C6125A669AEB005699 /* UserProfileScreenViewModelProtocol.swift in Sources */,
|
||||
69A9B430397C15075D86193F /* UserPropertiesExt.swift in Sources */,
|
||||
8AB8ED1051216546CB35FA0E /* UserSession.swift in Sources */,
|
||||
63129D147B94BAD61A2C1CA1 /* UserSessionDetails.swift in Sources */,
|
||||
4A618590DEB72C4F186BFED4 /* UserSessionFlowCoordinator.swift in Sources */,
|
||||
91C590378C591DCA3E40416A /* UserSessionHook.swift in Sources */,
|
||||
87DCBD9BC3A00D3E5849460B /* UserSessionManager.swift in Sources */,
|
||||
9FCE652911C1CDA6DC51924B /* UserSessionManagerProtocol.swift in Sources */,
|
||||
6586E1F1D5F0651D0638FFAF /* UserSessionMock.swift in Sources */,
|
||||
978BB24F2A5D31EE59EEC249 /* UserSessionProtocol.swift in Sources */,
|
||||
7E91BAC17963ED41208F489B /* UserSessionStore.swift in Sources */,
|
||||
|
||||
@@ -19,6 +19,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
private let stateMachine: AppCoordinatorStateMachine
|
||||
private let navigationRootCoordinator: NavigationRootCoordinator
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
private let userSessionManager: UserSessionManagerProtocol
|
||||
// periphery:ignore - retaining purpose
|
||||
private let targetConfiguration: Target.ConfigurationResult
|
||||
private let appMediator: AppMediator
|
||||
@@ -38,18 +39,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
private var userSessionMigrationsOldVersion: Version?
|
||||
private var userSession: UserSessionProtocol? {
|
||||
didSet {
|
||||
userSessionObserver?.cancel()
|
||||
if let userSession {
|
||||
configureElementCallService()
|
||||
configureNotificationManager()
|
||||
observeUserSessionChanges()
|
||||
Task {
|
||||
await resumeClientServices()
|
||||
await appHooks.configure(with: userSession)
|
||||
}
|
||||
}
|
||||
}
|
||||
userSessionManager.activeSession
|
||||
}
|
||||
|
||||
private var authenticationFlowCoordinator: AuthenticationFlowCoordinator?
|
||||
@@ -132,6 +122,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
analyticsService: analyticsService,
|
||||
appHooks: appHooks,
|
||||
networkMonitor: networkMonitor)
|
||||
userSessionManager = UserSessionManager(userSessionStore: userSessionStore, appSettings: appSettings)
|
||||
|
||||
let appLockService = AppLockService(keychainController: keychainController, appSettings: appSettings)
|
||||
let appLockNavigationCoordinator = NavigationRootCoordinator()
|
||||
@@ -216,7 +207,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
return
|
||||
}
|
||||
|
||||
guard userSessionStore.hasSessions else {
|
||||
guard !userSessionManager.userIDs.isEmpty else {
|
||||
stateMachine.processEvent(.startWithAuthentication)
|
||||
return
|
||||
}
|
||||
@@ -379,7 +370,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
// MARK: - AuthenticationFlowCoordinatorDelegate
|
||||
|
||||
func authenticationFlowCoordinator(didLoginWithSession userSession: UserSessionProtocol) {
|
||||
self.userSession = userSession
|
||||
userSessionManager.add(userSession)
|
||||
configureActiveSession()
|
||||
authenticationFlowCoordinator = nil
|
||||
stateMachine.processEvent(.createdUserSession)
|
||||
}
|
||||
@@ -580,7 +572,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
appSettings.resetAllSettings()
|
||||
appLockFlowCoordinator.appLockService.disable()
|
||||
}
|
||||
userSessionStore.reset()
|
||||
userSessionManager.reset()
|
||||
}
|
||||
|
||||
/// Manually cleans up any files in the app group's `tmp` directory.
|
||||
@@ -673,10 +665,11 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
private func restoreUserSession() {
|
||||
Task {
|
||||
switch await userSessionStore.restoreUserSession() {
|
||||
switch await userSessionManager.restoreActiveSession() {
|
||||
case .success(let userSession):
|
||||
await self.performUserSessionMigrations(userSession)
|
||||
self.userSession = userSession
|
||||
userSessionManager.add(userSession)
|
||||
configureActiveSession()
|
||||
stateMachine.processEvent(.createdUserSession)
|
||||
case .failure:
|
||||
MXLog.error("Failed to restore an existing session.")
|
||||
@@ -765,7 +758,8 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
|
||||
switch action {
|
||||
case .signedIn(let session):
|
||||
self.userSession = session
|
||||
self.userSessionManager.add(session)
|
||||
self.configureActiveSession()
|
||||
self.softLogoutCoordinator = nil
|
||||
stateMachine.processEvent(.createdUserSession)
|
||||
case .clearAllData:
|
||||
@@ -793,6 +787,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: userSessionManager.sessionsPublisher.map { $0.map(UserSessionDetails.init) },
|
||||
bugReportService: bugReportService,
|
||||
elementCallService: elementCallService,
|
||||
timelineControllerFactory: TimelineControllerFactory(userSettings: userSession.userSettings),
|
||||
@@ -867,7 +862,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
await userSession.clientProxy.logout()
|
||||
|
||||
// Regardless of the result, clear user data
|
||||
userSessionStore.logout(userSession: userSession)
|
||||
userSessionManager.remove(userID: userSession.clientProxy.userID)
|
||||
tearDownUserSession()
|
||||
|
||||
userSession.userSettings.account.reset()
|
||||
@@ -886,7 +881,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
private func tearDownUserSession() {
|
||||
userIndicatorController.retractAllIndicators()
|
||||
|
||||
userSession = nil
|
||||
userSessionObserver?.cancel()
|
||||
|
||||
userSessionFlowCoordinator = nil
|
||||
|
||||
@@ -912,15 +907,28 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
}
|
||||
|
||||
private func configureElementCallService() {
|
||||
/// Points the app-wide services at the active account's session.
|
||||
private func configureActiveSession() {
|
||||
guard let userSession else {
|
||||
fatalError("User session not setup")
|
||||
}
|
||||
|
||||
elementCallService.setUserSession(userSession)
|
||||
configureServices(for: userSession)
|
||||
Task {
|
||||
await resumeClientServices()
|
||||
await appHooks.configure(with: userSession)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureNotificationManager() {
|
||||
/// Wires the services that work for every signed in account, not only the active one. They only hold a single session for now.
|
||||
private func configureServices(for userSession: UserSessionProtocol) {
|
||||
userSessionObserver?.cancel()
|
||||
elementCallService.setUserSession(userSession)
|
||||
configureNotificationManager(for: userSession)
|
||||
observeUserSessionChanges(userSession)
|
||||
}
|
||||
|
||||
private func configureNotificationManager(for userSession: UserSessionProtocol) {
|
||||
notificationManager.setUserSession(userSession)
|
||||
|
||||
appDelegateObserver = appDelegate.callbacks
|
||||
@@ -935,11 +943,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
||||
}
|
||||
}
|
||||
|
||||
private func observeUserSessionChanges() {
|
||||
guard let userSession else {
|
||||
fatalError("User session not setup")
|
||||
}
|
||||
|
||||
private func observeUserSessionChanges(_ userSession: UserSessionProtocol) {
|
||||
userSessionObserver = userSession.callbacks
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
@@ -1354,7 +1358,7 @@ private extension AppCoordinator {
|
||||
/// When iOS relaunches a terminated app for a background task, the session needs restoring before the task can run.
|
||||
private func waitForSessionRestore() async {
|
||||
// A background launch doesn't necessarily connect a scene, which is what normally calls `start()`.
|
||||
if stateMachine.state == .initial, userSessionStore.hasSessions {
|
||||
if stateMachine.state == .initial, !userSessionManager.userIDs.isEmpty {
|
||||
start()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,10 @@ extension FlowCoordinatorProtocol {
|
||||
///
|
||||
/// Please do **not** pass this type directly to screen coordinators/view models.
|
||||
struct CommonFlowParameters {
|
||||
/// The session of the account this flow tree belongs to. It never changes, even when another account becomes active.
|
||||
let userSession: UserSessionProtocol
|
||||
/// Every signed in account, most recently active first, so the first one is `userSession`'s.
|
||||
let availableSessionsPublisher: CurrentValuePublisher<[UserSessionDetails], Never>
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let elementCallService: ElementCallServiceProtocol
|
||||
let timelineControllerFactory: TimelineControllerFactoryProtocol
|
||||
|
||||
@@ -14,6 +14,12 @@ final nonisolated class AccountSettings: @unchecked Sendable {
|
||||
private let store: AccountUserDefaults
|
||||
private let userID: String
|
||||
|
||||
// MARK: Accounts
|
||||
|
||||
/// When this account was last selected, used to order the accounts most recently selected first.
|
||||
@UserPreference
|
||||
var lastSelectedDate: Date?
|
||||
|
||||
// MARK: Session
|
||||
|
||||
@UserPreference(defaultValue: false)
|
||||
|
||||
@@ -375,6 +375,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
private func presentHomeScreen() {
|
||||
let parameters = HomeScreenCoordinatorParameters(userSession: userSession,
|
||||
availableSessionsPublisher: flowParameters.availableSessionsPublisher,
|
||||
bugReportService: flowParameters.bugReportService,
|
||||
selectedRoomPublisher: selectedRoomSubject.asCurrentValuePublisher(),
|
||||
analyticsService: flowParameters.analytics,
|
||||
|
||||
@@ -14810,6 +14810,165 @@ nonisolated class UserNotificationCenterMock: UserNotificationCenterProtocol, @u
|
||||
}
|
||||
}
|
||||
}
|
||||
nonisolated class UserSessionManagerMock: UserSessionManagerProtocol, @unchecked Sendable {
|
||||
nonisolated(unsafe) var userIDs: [String] = []
|
||||
var sessionsPublisher: CurrentValuePublisher<[UserSessionProtocol], Never> {
|
||||
get { return underlyingSessionsPublisher }
|
||||
set(value) { underlyingSessionsPublisher = value }
|
||||
}
|
||||
nonisolated(unsafe) var underlyingSessionsPublisher: CurrentValuePublisher<[UserSessionProtocol], Never>!
|
||||
nonisolated(unsafe) var activeSession: UserSessionProtocol?
|
||||
|
||||
//MARK: - session
|
||||
|
||||
private let sessionForCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var sessionForUnderlyingCallsCount = 0
|
||||
var sessionForCallsCount: Int {
|
||||
get { sessionForCallsCountLock.withLock { sessionForUnderlyingCallsCount } }
|
||||
set { sessionForCallsCountLock.withLock { sessionForUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var sessionForCalled: Bool {
|
||||
return sessionForCallsCount > 0
|
||||
}
|
||||
private let sessionForReceivedUserIDLock = NSLock()
|
||||
private nonisolated(unsafe) var sessionForUnderlyingReceivedUserID: String?
|
||||
var sessionForReceivedUserID: String? {
|
||||
get { sessionForReceivedUserIDLock.withLock { sessionForUnderlyingReceivedUserID } }
|
||||
set { sessionForReceivedUserIDLock.withLock { sessionForUnderlyingReceivedUserID = newValue } }
|
||||
}
|
||||
private let sessionForReceivedInvocationsLock = NSLock()
|
||||
private nonisolated(unsafe) var sessionForUnderlyingReceivedInvocations: [String] = []
|
||||
var sessionForReceivedInvocations: [String] {
|
||||
get { sessionForReceivedInvocationsLock.withLock { sessionForUnderlyingReceivedInvocations } }
|
||||
set { sessionForReceivedInvocationsLock.withLock { sessionForUnderlyingReceivedInvocations = newValue } }
|
||||
}
|
||||
|
||||
private let sessionForReturnValueLock = NSLock()
|
||||
private nonisolated(unsafe) var sessionForUnderlyingReturnValue: UserSessionProtocol?
|
||||
var sessionForReturnValue: UserSessionProtocol? {
|
||||
get { sessionForReturnValueLock.withLock { sessionForUnderlyingReturnValue } }
|
||||
set { sessionForReturnValueLock.withLock { sessionForUnderlyingReturnValue = newValue } }
|
||||
}
|
||||
nonisolated(unsafe) var sessionForClosure: ((String) -> UserSessionProtocol?)?
|
||||
|
||||
func session(for userID: String) -> UserSessionProtocol? {
|
||||
sessionForCallsCountLock.withLock { sessionForUnderlyingCallsCount += 1 }
|
||||
sessionForReceivedUserID = userID
|
||||
sessionForReceivedInvocationsLock.withLock { sessionForUnderlyingReceivedInvocations.append(userID) }
|
||||
if let sessionForClosure = sessionForClosure {
|
||||
return sessionForClosure(userID)
|
||||
} else {
|
||||
return sessionForReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - restoreActiveSession
|
||||
|
||||
private let restoreActiveSessionCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreActiveSessionUnderlyingCallsCount = 0
|
||||
var restoreActiveSessionCallsCount: Int {
|
||||
get { restoreActiveSessionCallsCountLock.withLock { restoreActiveSessionUnderlyingCallsCount } }
|
||||
set { restoreActiveSessionCallsCountLock.withLock { restoreActiveSessionUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var restoreActiveSessionCalled: Bool {
|
||||
return restoreActiveSessionCallsCount > 0
|
||||
}
|
||||
|
||||
private let restoreActiveSessionReturnValueLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreActiveSessionUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionManagerError>!
|
||||
var restoreActiveSessionReturnValue: Result<UserSessionProtocol, UserSessionManagerError>! {
|
||||
get { restoreActiveSessionReturnValueLock.withLock { restoreActiveSessionUnderlyingReturnValue } }
|
||||
set { restoreActiveSessionReturnValueLock.withLock { restoreActiveSessionUnderlyingReturnValue = newValue } }
|
||||
}
|
||||
nonisolated(unsafe) var restoreActiveSessionClosure: (() async -> Result<UserSessionProtocol, UserSessionManagerError>)?
|
||||
|
||||
@concurrent func restoreActiveSession() async -> Result<UserSessionProtocol, UserSessionManagerError> {
|
||||
restoreActiveSessionCallsCountLock.withLock { restoreActiveSessionUnderlyingCallsCount += 1 }
|
||||
if let restoreActiveSessionClosure = restoreActiveSessionClosure {
|
||||
return await restoreActiveSessionClosure()
|
||||
} else {
|
||||
return restoreActiveSessionReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - add
|
||||
|
||||
private let addCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var addUnderlyingCallsCount = 0
|
||||
var addCallsCount: Int {
|
||||
get { addCallsCountLock.withLock { addUnderlyingCallsCount } }
|
||||
set { addCallsCountLock.withLock { addUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var addCalled: Bool {
|
||||
return addCallsCount > 0
|
||||
}
|
||||
private let addReceivedUserSessionLock = NSLock()
|
||||
private nonisolated(unsafe) var addUnderlyingReceivedUserSession: UserSessionProtocol?
|
||||
var addReceivedUserSession: UserSessionProtocol? {
|
||||
get { addReceivedUserSessionLock.withLock { addUnderlyingReceivedUserSession } }
|
||||
set { addReceivedUserSessionLock.withLock { addUnderlyingReceivedUserSession = newValue } }
|
||||
}
|
||||
private let addReceivedInvocationsLock = NSLock()
|
||||
private nonisolated(unsafe) var addUnderlyingReceivedInvocations: [UserSessionProtocol] = []
|
||||
var addReceivedInvocations: [UserSessionProtocol] {
|
||||
get { addReceivedInvocationsLock.withLock { addUnderlyingReceivedInvocations } }
|
||||
set { addReceivedInvocationsLock.withLock { addUnderlyingReceivedInvocations = newValue } }
|
||||
}
|
||||
nonisolated(unsafe) var addClosure: ((UserSessionProtocol) -> Void)?
|
||||
|
||||
func add(_ userSession: UserSessionProtocol) {
|
||||
addCallsCountLock.withLock { addUnderlyingCallsCount += 1 }
|
||||
addReceivedUserSession = userSession
|
||||
addReceivedInvocationsLock.withLock { addUnderlyingReceivedInvocations.append(userSession) }
|
||||
addClosure?(userSession)
|
||||
}
|
||||
//MARK: - remove
|
||||
|
||||
private let removeUserIDCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var removeUserIDUnderlyingCallsCount = 0
|
||||
var removeUserIDCallsCount: Int {
|
||||
get { removeUserIDCallsCountLock.withLock { removeUserIDUnderlyingCallsCount } }
|
||||
set { removeUserIDCallsCountLock.withLock { removeUserIDUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var removeUserIDCalled: Bool {
|
||||
return removeUserIDCallsCount > 0
|
||||
}
|
||||
private let removeUserIDReceivedUserIDLock = NSLock()
|
||||
private nonisolated(unsafe) var removeUserIDUnderlyingReceivedUserID: String?
|
||||
var removeUserIDReceivedUserID: String? {
|
||||
get { removeUserIDReceivedUserIDLock.withLock { removeUserIDUnderlyingReceivedUserID } }
|
||||
set { removeUserIDReceivedUserIDLock.withLock { removeUserIDUnderlyingReceivedUserID = newValue } }
|
||||
}
|
||||
private let removeUserIDReceivedInvocationsLock = NSLock()
|
||||
private nonisolated(unsafe) var removeUserIDUnderlyingReceivedInvocations: [String] = []
|
||||
var removeUserIDReceivedInvocations: [String] {
|
||||
get { removeUserIDReceivedInvocationsLock.withLock { removeUserIDUnderlyingReceivedInvocations } }
|
||||
set { removeUserIDReceivedInvocationsLock.withLock { removeUserIDUnderlyingReceivedInvocations = newValue } }
|
||||
}
|
||||
nonisolated(unsafe) var removeUserIDClosure: ((String) -> Void)?
|
||||
|
||||
func remove(userID: String) {
|
||||
removeUserIDCallsCountLock.withLock { removeUserIDUnderlyingCallsCount += 1 }
|
||||
removeUserIDReceivedUserID = userID
|
||||
removeUserIDReceivedInvocationsLock.withLock { removeUserIDUnderlyingReceivedInvocations.append(userID) }
|
||||
removeUserIDClosure?(userID)
|
||||
}
|
||||
//MARK: - reset
|
||||
|
||||
private let resetCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var resetUnderlyingCallsCount = 0
|
||||
var resetCallsCount: Int {
|
||||
get { resetCallsCountLock.withLock { resetUnderlyingCallsCount } }
|
||||
set { resetCallsCountLock.withLock { resetUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var resetCalled: Bool {
|
||||
return resetCallsCount > 0
|
||||
}
|
||||
nonisolated(unsafe) var resetClosure: (() -> Void)?
|
||||
|
||||
func reset() {
|
||||
resetCallsCountLock.withLock { resetUnderlyingCallsCount += 1 }
|
||||
resetClosure?()
|
||||
}
|
||||
}
|
||||
nonisolated class UserSessionMock: UserSessionProtocol, @unchecked Sendable {
|
||||
var clientProxy: ClientProxyProtocol {
|
||||
get { return underlyingClientProxy }
|
||||
@@ -14881,30 +15040,44 @@ nonisolated class UserSessionStoreMock: UserSessionStoreProtocol, @unchecked Sen
|
||||
}
|
||||
//MARK: - restoreUserSession
|
||||
|
||||
private let restoreUserSessionCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreUserSessionUnderlyingCallsCount = 0
|
||||
var restoreUserSessionCallsCount: Int {
|
||||
get { restoreUserSessionCallsCountLock.withLock { restoreUserSessionUnderlyingCallsCount } }
|
||||
set { restoreUserSessionCallsCountLock.withLock { restoreUserSessionUnderlyingCallsCount = newValue } }
|
||||
private let restoreUserSessionUserIDCallsCountLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreUserSessionUserIDUnderlyingCallsCount = 0
|
||||
var restoreUserSessionUserIDCallsCount: Int {
|
||||
get { restoreUserSessionUserIDCallsCountLock.withLock { restoreUserSessionUserIDUnderlyingCallsCount } }
|
||||
set { restoreUserSessionUserIDCallsCountLock.withLock { restoreUserSessionUserIDUnderlyingCallsCount = newValue } }
|
||||
}
|
||||
var restoreUserSessionCalled: Bool {
|
||||
return restoreUserSessionCallsCount > 0
|
||||
var restoreUserSessionUserIDCalled: Bool {
|
||||
return restoreUserSessionUserIDCallsCount > 0
|
||||
}
|
||||
private let restoreUserSessionUserIDReceivedUserIDLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreUserSessionUserIDUnderlyingReceivedUserID: String?
|
||||
var restoreUserSessionUserIDReceivedUserID: String? {
|
||||
get { restoreUserSessionUserIDReceivedUserIDLock.withLock { restoreUserSessionUserIDUnderlyingReceivedUserID } }
|
||||
set { restoreUserSessionUserIDReceivedUserIDLock.withLock { restoreUserSessionUserIDUnderlyingReceivedUserID = newValue } }
|
||||
}
|
||||
private let restoreUserSessionUserIDReceivedInvocationsLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreUserSessionUserIDUnderlyingReceivedInvocations: [String] = []
|
||||
var restoreUserSessionUserIDReceivedInvocations: [String] {
|
||||
get { restoreUserSessionUserIDReceivedInvocationsLock.withLock { restoreUserSessionUserIDUnderlyingReceivedInvocations } }
|
||||
set { restoreUserSessionUserIDReceivedInvocationsLock.withLock { restoreUserSessionUserIDUnderlyingReceivedInvocations = newValue } }
|
||||
}
|
||||
|
||||
private let restoreUserSessionReturnValueLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreUserSessionUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
|
||||
var restoreUserSessionReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
|
||||
get { restoreUserSessionReturnValueLock.withLock { restoreUserSessionUnderlyingReturnValue } }
|
||||
set { restoreUserSessionReturnValueLock.withLock { restoreUserSessionUnderlyingReturnValue = newValue } }
|
||||
private let restoreUserSessionUserIDReturnValueLock = NSLock()
|
||||
private nonisolated(unsafe) var restoreUserSessionUserIDUnderlyingReturnValue: Result<UserSessionProtocol, UserSessionStoreError>!
|
||||
var restoreUserSessionUserIDReturnValue: Result<UserSessionProtocol, UserSessionStoreError>! {
|
||||
get { restoreUserSessionUserIDReturnValueLock.withLock { restoreUserSessionUserIDUnderlyingReturnValue } }
|
||||
set { restoreUserSessionUserIDReturnValueLock.withLock { restoreUserSessionUserIDUnderlyingReturnValue = newValue } }
|
||||
}
|
||||
nonisolated(unsafe) var restoreUserSessionClosure: (() async -> Result<UserSessionProtocol, UserSessionStoreError>)?
|
||||
nonisolated(unsafe) var restoreUserSessionUserIDClosure: ((String) async -> Result<UserSessionProtocol, UserSessionStoreError>)?
|
||||
|
||||
@concurrent func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
restoreUserSessionCallsCountLock.withLock { restoreUserSessionUnderlyingCallsCount += 1 }
|
||||
if let restoreUserSessionClosure = restoreUserSessionClosure {
|
||||
return await restoreUserSessionClosure()
|
||||
@concurrent func restoreUserSession(userID: String) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
restoreUserSessionUserIDCallsCountLock.withLock { restoreUserSessionUserIDUnderlyingCallsCount += 1 }
|
||||
restoreUserSessionUserIDReceivedUserID = userID
|
||||
restoreUserSessionUserIDReceivedInvocationsLock.withLock { restoreUserSessionUserIDUnderlyingReceivedInvocations.append(userID) }
|
||||
if let restoreUserSessionUserIDClosure = restoreUserSessionUserIDClosure {
|
||||
return await restoreUserSessionUserIDClosure(userID)
|
||||
} else {
|
||||
return restoreUserSessionReturnValue
|
||||
return restoreUserSessionUserIDReturnValue
|
||||
}
|
||||
}
|
||||
//MARK: - userSession
|
||||
|
||||
@@ -11,6 +11,7 @@ import SwiftUI
|
||||
|
||||
struct HomeScreenCoordinatorParameters {
|
||||
let userSession: UserSessionProtocol
|
||||
let availableSessionsPublisher: CurrentValuePublisher<[UserSessionDetails], Never>
|
||||
let bugReportService: BugReportServiceProtocol
|
||||
let selectedRoomPublisher: CurrentValuePublisher<String?, Never>
|
||||
let analyticsService: AnalyticsServiceProtocol
|
||||
@@ -48,6 +49,7 @@ final class HomeScreenCoordinator: CoordinatorProtocol {
|
||||
|
||||
init(parameters: HomeScreenCoordinatorParameters) {
|
||||
viewModel = HomeScreenViewModel(userSession: parameters.userSession,
|
||||
availableSessionsPublisher: parameters.availableSessionsPublisher,
|
||||
selectedRoomPublisher: parameters.selectedRoomPublisher,
|
||||
analyticsService: parameters.analyticsService,
|
||||
bugReportService: parameters.bugReportService,
|
||||
|
||||
@@ -15,6 +15,7 @@ typealias HomeScreenViewModelType = StateStoreViewModel<HomeScreenViewState, Hom
|
||||
|
||||
class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol {
|
||||
private let userSession: UserSessionProtocol
|
||||
private let availableSessionsPublisher: CurrentValuePublisher<[UserSessionDetails], Never>
|
||||
private let spaceFilterSubject: CurrentValueSubject<SpaceServiceFilter?, Never>
|
||||
private let analyticsService: AnalyticsServiceProtocol
|
||||
private let bugReportService: BugReportServiceProtocol
|
||||
@@ -31,12 +32,14 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
// swiftlint:disable:next function_body_length
|
||||
init(userSession: UserSessionProtocol,
|
||||
availableSessionsPublisher: CurrentValuePublisher<[UserSessionDetails], Never>,
|
||||
selectedRoomPublisher: CurrentValuePublisher<String?, Never>,
|
||||
analyticsService: AnalyticsServiceProtocol,
|
||||
bugReportService: BugReportServiceProtocol,
|
||||
notificationManager: NotificationManagerProtocol,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.userSession = userSession
|
||||
self.availableSessionsPublisher = availableSessionsPublisher
|
||||
self.analyticsService = analyticsService
|
||||
self.bugReportService = bugReportService
|
||||
userSettings = userSession.userSettings
|
||||
@@ -486,6 +489,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
// MARK: Multi-account announcement
|
||||
|
||||
private func presentMultiAccountAnnouncementIfNeeded() {
|
||||
// Never announce it to someone who already uses several accounts, even once they've removed some.
|
||||
if availableSessionsPublisher.value.count > 1, !userSettings.app.hasSeenMultiAccountAnnouncement {
|
||||
MXLog.info("Several accounts are signed in, marking the multi-account announcement as seen.")
|
||||
userSettings.app.hasSeenMultiAccountAnnouncement = true
|
||||
}
|
||||
|
||||
// An unverified session gets the identity confirmation cover first, the room list appears again once it's dismissed.
|
||||
guard userSettings.app.multiAccountEnabled,
|
||||
!userSettings.app.hasSeenMultiAccountAnnouncement,
|
||||
|
||||
@@ -236,6 +236,7 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -144,6 +144,7 @@ struct HomeScreenEmptyStateView_Previews: PreviewProvider, TestablePreview {
|
||||
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@user:example.com",
|
||||
roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loaded([])))))))
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -200,6 +200,7 @@ struct HomeScreenInviteCell_Previews: PreviewProvider, TestablePreview {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -132,6 +132,7 @@ struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -121,6 +121,7 @@ struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, Testab
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
/// What a flow can know about a signed in account, without access to its session.
|
||||
struct UserSessionDetails {
|
||||
let userID: String
|
||||
}
|
||||
|
||||
extension UserSessionDetails {
|
||||
init(userSession: UserSessionProtocol) {
|
||||
userID = userSession.clientProxy.userID
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import OrderedCollections
|
||||
|
||||
/// Owns the live session of every account signed in on this device, most recently active first.
|
||||
///
|
||||
/// Only the `AppCoordinator` calls the lifecycle methods, so the active account always changes
|
||||
/// through its state machine. Flows only receive the `UserSessionDetails` of its sessions.
|
||||
final class UserSessionManager: UserSessionManagerProtocol {
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
private let appSettings: AppSettings
|
||||
|
||||
private let sessionsSubject = CurrentValueSubject<[UserSessionProtocol], Never>([])
|
||||
|
||||
/// Every signed in account, most recently selected first, with its session once it's live.
|
||||
/// Assigning `nil` removes an account rather than clearing its session.
|
||||
private var accounts: OrderedDictionary<String, UserSessionProtocol?> = [:] {
|
||||
didSet { sessionsSubject.send(accounts.values.compactMap { $0 }) }
|
||||
}
|
||||
|
||||
var userIDs: [String] {
|
||||
Array(accounts.keys)
|
||||
}
|
||||
|
||||
var sessionsPublisher: CurrentValuePublisher<[UserSessionProtocol], Never> {
|
||||
sessionsSubject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
var activeSession: UserSessionProtocol? {
|
||||
accounts.values.first ?? nil
|
||||
}
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol, appSettings: AppSettings) {
|
||||
self.userSessionStore = userSessionStore
|
||||
self.appSettings = appSettings
|
||||
|
||||
let userIDs = userSessionStore.userIDs
|
||||
.map { (userID: $0, lastSelectedDate: appSettings.userSettings(for: $0).account.lastSelectedDate) }
|
||||
.sorted { lhs, rhs in
|
||||
switch (lhs.lastSelectedDate, rhs.lastSelectedDate) {
|
||||
case let (lhsDate?, rhsDate?) where lhsDate != rhsDate: lhsDate > rhsDate
|
||||
case (.some, .none): true
|
||||
case (.none, .some): false
|
||||
default: lhs.userID < rhs.userID
|
||||
}
|
||||
}
|
||||
.map(\.userID)
|
||||
accounts = OrderedDictionary(uniqueKeysWithValues: userIDs.map { ($0, UserSessionProtocol?.none) })
|
||||
}
|
||||
|
||||
func session(for userID: String) -> UserSessionProtocol? {
|
||||
accounts[userID] ?? nil
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
func restoreActiveSession() async -> Result<UserSessionProtocol, UserSessionManagerError> {
|
||||
guard !accounts.isEmpty else {
|
||||
return .failure(.noAccounts)
|
||||
}
|
||||
|
||||
while let userID = accounts.keys.first {
|
||||
switch await userSessionStore.restoreUserSession(userID: userID) {
|
||||
case .success(let userSession):
|
||||
return .success(userSession)
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed restoring \(userID), falling back to the next account: \(error)")
|
||||
accounts.removeValue(forKey: userID)
|
||||
}
|
||||
}
|
||||
|
||||
return .failure(.failedRestoringSessions)
|
||||
}
|
||||
|
||||
func add(_ userSession: UserSessionProtocol) {
|
||||
let userID = userSession.clientProxy.userID
|
||||
|
||||
if accounts.keys.contains(userID) {
|
||||
accounts[userID] = userSession
|
||||
} else {
|
||||
appSettings.userSettings(for: userID).account.lastSelectedDate = .now
|
||||
accounts.updateValue(userSession, forKey: userID, insertingAt: 0)
|
||||
}
|
||||
}
|
||||
|
||||
func remove(userID: String) {
|
||||
guard let userSession = accounts[userID] ?? nil else {
|
||||
MXLog.error("Tried to remove \(userID), which doesn't have a live session.")
|
||||
return
|
||||
}
|
||||
|
||||
userSessionStore.logout(userSession: userSession)
|
||||
accounts.removeValue(forKey: userID)
|
||||
}
|
||||
|
||||
func reset() {
|
||||
userSessionStore.reset()
|
||||
accounts.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
enum UserSessionManagerError: Error {
|
||||
/// There are no signed in accounts to restore.
|
||||
case noAccounts
|
||||
/// None of the signed in accounts could be restored, so they have all been removed.
|
||||
case failedRestoringSessions
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol UserSessionManagerProtocol: AnyObject {
|
||||
/// Every signed in account, most recently active first. Known before any session is live, so use it to count accounts.
|
||||
var userIDs: [String] { get }
|
||||
/// The live sessions, in the same order as `userIDs`.
|
||||
var sessionsPublisher: CurrentValuePublisher<[UserSessionProtocol], Never> { get }
|
||||
/// The session of the account whose UI is on screen.
|
||||
var activeSession: UserSessionProtocol? { get }
|
||||
|
||||
func session(for userID: String) -> UserSessionProtocol?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Restores the most recently active account, falling back to the next one when it fails (the store deletes failed accounts).
|
||||
///
|
||||
/// The session isn't registered: `add` it once it's ready to be used, e.g. after any migrations.
|
||||
func restoreActiveSession() async -> Result<UserSessionProtocol, UserSessionManagerError>
|
||||
/// Registers a session that's ready to be used, replacing any previous session for its account.
|
||||
/// A new account becomes the active one, a known account keeps its place.
|
||||
func add(_ userSession: UserSessionProtocol)
|
||||
/// Deletes an account's credentials and data, and forgets its session.
|
||||
func remove(userID: String)
|
||||
/// Deletes every account's credentials and data.
|
||||
func reset()
|
||||
}
|
||||
@@ -52,11 +52,9 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
keychainController.removeAllRestorationTokens()
|
||||
}
|
||||
|
||||
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
let availableCredentials = keychainController.restorationTokens()
|
||||
|
||||
guard let credentials = availableCredentials.first else {
|
||||
return .failure(.missingCredentials)
|
||||
func restoreUserSession(userID: String) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
guard let credentials = keychainController.restorationTokens().first(where: { $0.userID == userID }) else {
|
||||
return .failure(.unknownAccount)
|
||||
}
|
||||
|
||||
let userSettings = appSettings.userSettings(for: credentials.userID)
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
enum UserSessionStoreError: Error {
|
||||
case missingCredentials
|
||||
case unknownAccount
|
||||
case failedRestoringLogin
|
||||
case failedSettingUpSession
|
||||
case failedSettingUpClientProxy(Error)
|
||||
@@ -30,8 +30,8 @@ protocol UserSessionStoreProtocol {
|
||||
/// Returns the delegate that should handle any changes to a `Client`'s `Session`.
|
||||
var clientSessionDelegate: ClientSessionDelegate { get }
|
||||
|
||||
/// Restores an existing user session.
|
||||
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
/// Restores the session of the given account. If this fails, the account's credentials and data are deleted.
|
||||
func restoreUserSession(userID: String) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// Creates a user session for a new client from the SDK along with the passphrase used for the data stores.
|
||||
func userSession(for client: ClientProtocol, sessionDirectories: SessionDirectories, passphrase: Data) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
@@ -640,6 +640,7 @@ class MockScreen: Identifiable {
|
||||
appLockService: AppLockService(keychainController: KeychainControllerMock(),
|
||||
appSettings: appSettings),
|
||||
flowParameters: CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
@@ -692,6 +693,7 @@ class MockScreen: Identifiable {
|
||||
userDiscoveryService: userDiscoveryService,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
flowParameters: CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
@@ -792,6 +794,7 @@ class MockScreen: Identifiable {
|
||||
let flowCoordinator = LinkNewDeviceFlowCoordinator(navigationStackCoordinator: navigationStackCoordinator,
|
||||
appLockService: appLockService,
|
||||
flowParameters: CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
@@ -848,6 +851,7 @@ class MockScreen: Identifiable {
|
||||
|
||||
let flowCoordinator = ChatsTabFlowCoordinator(navigationSplitCoordinator: navigationSplitCoordinator,
|
||||
flowParameters: CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init(timelineController: timelineController)),
|
||||
|
||||
@@ -38,6 +38,7 @@ struct ChatsTabFlowCoordinatorTests {
|
||||
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
|
||||
@@ -460,6 +460,19 @@ final class HomeScreenViewModelTests {
|
||||
try await deferred.fulfill()
|
||||
}
|
||||
|
||||
@Test
|
||||
func multiAccountAnnouncementIsMarkedSeenWithSeveralAccounts() async throws {
|
||||
userSettings.app.multiAccountEnabled = true
|
||||
userSettings.app.hasSeenMultiAccountAnnouncement = false
|
||||
setupViewModel(otherAccountUserIDs: ["@other:client.com"])
|
||||
|
||||
let deferred = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.bindings.isPresentingMultiAccountAnnouncement }
|
||||
context.send(viewAction: .screenAppeared)
|
||||
try await deferred.fulfill()
|
||||
|
||||
#expect(userSettings.app.hasSeenMultiAccountAnnouncement)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
enum InviteType { case rooms, spaces }
|
||||
@@ -551,7 +564,7 @@ final class HomeScreenViewModelTests {
|
||||
return (roomListSubject, stateSubject)
|
||||
}
|
||||
|
||||
private func setupViewModel(securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil, roomSummaryProvider: RoomSummaryProviderMock? = nil) {
|
||||
private func setupViewModel(otherAccountUserIDs: [String] = [], securityStatePublisher: CurrentValuePublisher<SessionSecurityState, Never>? = nil, invites: InviteType? = nil, roomSummaryProvider: RoomSummaryProviderMock? = nil) {
|
||||
cancellables.removeAll()
|
||||
|
||||
var rooms: [RoomSummary] = .mockRooms
|
||||
@@ -595,7 +608,10 @@ final class HomeScreenViewModelTests {
|
||||
|
||||
notificationManager = NotificationManagerMock()
|
||||
|
||||
let sessionDetails = [UserSessionDetails(userSession: userSession)] + otherAccountUserIDs.map { UserSessionDetails(userID: $0) }
|
||||
|
||||
viewModel = HomeScreenViewModel(userSession: userSession,
|
||||
availableSessionsPublisher: .init(sessionDetails),
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
analyticsService: AnalyticsServiceMock(.init()),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
|
||||
@@ -522,7 +522,9 @@ final class RoomFlowCoordinatorTests {
|
||||
"1"
|
||||
}
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings)),
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy, userSettings: userSettings))
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: timelineControllerFactory,
|
||||
|
||||
@@ -54,6 +54,7 @@ struct RoomMembersFlowCoordinatorTests {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
|
||||
@@ -51,6 +51,7 @@ struct UserSessionFlowCoordinatorTests {
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
let flowParameters = CommonFlowParameters(userSession: userSession,
|
||||
availableSessionsPublisher: .init([.init(userSession: userSession)]),
|
||||
bugReportService: BugReportServiceMock(.init()),
|
||||
elementCallService: ElementCallServiceMock(.init()),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()),
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// Copyright 2026 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
@testable import ElementX
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
struct UserSessionManagerTests {
|
||||
let userSessionStore = UserSessionStoreMock()
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
@Test
|
||||
func ordersAccountsByWhenTheyWereLastSelected() {
|
||||
appSettings.userSettings(for: "@alice:matrix.org").account.lastSelectedDate = .now.addingTimeInterval(-60)
|
||||
appSettings.userSettings(for: "@bob:matrix.org").account.lastSelectedDate = .now
|
||||
|
||||
// Accounts that were never selected (e.g. after upgrading from a single account) come last.
|
||||
let manager = makeManager(userIDs: ["@dave:matrix.org", "@alice:matrix.org", "@carol:matrix.org", "@bob:matrix.org"])
|
||||
|
||||
#expect(manager.userIDs == ["@bob:matrix.org", "@alice:matrix.org", "@carol:matrix.org", "@dave:matrix.org"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func restoreFallsBackToTheNextAccount() async {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
// Alice already has a live session, e.g. when restoring again after clearing the cache.
|
||||
manager.add(makeUserSession(userID: "@alice:matrix.org"))
|
||||
let bob = makeUserSession(userID: "@bob:matrix.org")
|
||||
userSessionStore.restoreUserSessionUserIDClosure = { userID in
|
||||
userID == "@bob:matrix.org" ? .success(bob) : .failure(.failedRestoringLogin)
|
||||
}
|
||||
|
||||
guard case .success(let userSession) = await manager.restoreActiveSession() else {
|
||||
Issue.record("Restoring should fall back to Bob.")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(userSession.clientProxy.userID == "@bob:matrix.org")
|
||||
#expect(manager.userIDs == ["@bob:matrix.org"])
|
||||
#expect(manager.session(for: "@alice:matrix.org") == nil)
|
||||
// Nothing can use the session until it's added, e.g. while migrations run.
|
||||
#expect(manager.activeSession == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func restoreFailsWhenNoAccountCanBeRestored() async {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
userSessionStore.restoreUserSessionUserIDReturnValue = .failure(.failedRestoringLogin)
|
||||
|
||||
guard case .failure(.failedRestoringSessions) = await manager.restoreActiveSession() else {
|
||||
Issue.record("Restoring should fail once every account has failed.")
|
||||
return
|
||||
}
|
||||
|
||||
#expect(userSessionStore.restoreUserSessionUserIDReceivedInvocations == ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect(manager.userIDs.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func addRegistersTheSession() {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
|
||||
// A known account keeps its place, e.g. after clearing the cache.
|
||||
let bob = makeUserSession(userID: "@bob:matrix.org")
|
||||
manager.add(bob)
|
||||
#expect(manager.userIDs == ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect((manager.session(for: "@bob:matrix.org") as? UserSessionMock) === bob)
|
||||
#expect(manager.activeSession == nil)
|
||||
|
||||
// A new account becomes the active one.
|
||||
let carol = makeUserSession(userID: "@carol:matrix.org")
|
||||
manager.add(carol)
|
||||
#expect(manager.userIDs == ["@carol:matrix.org", "@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect(appSettings.userSettings(for: "@carol:matrix.org").account.lastSelectedDate != nil)
|
||||
#expect((manager.activeSession as? UserSessionMock) === carol)
|
||||
#expect(manager.sessionsPublisher.value.map(\.clientProxy.userID) == ["@carol:matrix.org", "@bob:matrix.org"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func addReplacesTheSessionOfAKnownAccount() {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
manager.add(makeUserSession(userID: "@alice:matrix.org"))
|
||||
|
||||
// E.g. restoring again after clearing the cache, or signing in again after a soft logout.
|
||||
let newAlice = makeUserSession(userID: "@alice:matrix.org")
|
||||
manager.add(newAlice)
|
||||
|
||||
#expect(manager.userIDs == ["@alice:matrix.org", "@bob:matrix.org"])
|
||||
#expect((manager.activeSession as? UserSessionMock) === newAlice)
|
||||
}
|
||||
|
||||
@Test
|
||||
func removeDeletesTheAccount() {
|
||||
let manager = makeManager(userIDs: ["@alice:matrix.org"])
|
||||
let alice = makeUserSession(userID: "@alice:matrix.org")
|
||||
manager.add(alice)
|
||||
|
||||
manager.remove(userID: "@alice:matrix.org")
|
||||
|
||||
#expect((userSessionStore.logoutUserSessionReceivedUserSession as? UserSessionMock) === alice)
|
||||
#expect(manager.userIDs.isEmpty)
|
||||
#expect(manager.activeSession == nil)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func makeManager(userIDs: [String]) -> UserSessionManager {
|
||||
userSessionStore.userIDs = userIDs
|
||||
return UserSessionManager(userSessionStore: userSessionStore, appSettings: appSettings)
|
||||
}
|
||||
|
||||
private func makeUserSession(userID: String) -> UserSessionMock {
|
||||
UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: userID))))
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ struct UserSessionStoreTests {
|
||||
// MARK: - Restoration
|
||||
|
||||
@Test
|
||||
func restoreWithoutCredentials() async {
|
||||
guard case .failure(.missingCredentials) = await store.restoreUserSession() else {
|
||||
func restoreUnknownAccount() async {
|
||||
guard case .failure(.unknownAccount) = await store.restoreUserSession(userID: "@alice:matrix.org") else {
|
||||
Issue.record("Restoration should fail when there are no credentials.")
|
||||
return
|
||||
}
|
||||
@@ -60,7 +60,7 @@ struct UserSessionStoreTests {
|
||||
keychainController.restorationTokensReturnValue = [credentials]
|
||||
|
||||
// When restoring the session.
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession() else {
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession(userID: credentials.userID) else {
|
||||
Issue.record("Restoration should fail when the non-transient user data is missing.")
|
||||
return
|
||||
}
|
||||
@@ -79,7 +79,7 @@ struct UserSessionStoreTests {
|
||||
clientFactory.makeAppClientCredentialsClientSessionDelegateAppSettingsAppHooksThrowableError = TestError.generic
|
||||
|
||||
// When restoring the session.
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession() else {
|
||||
guard case .failure(.failedRestoringLogin) = await store.restoreUserSession(userID: credentials.userID) else {
|
||||
Issue.record("Restoration should fail when the client can't be created.")
|
||||
return
|
||||
}
|
||||
@@ -90,18 +90,19 @@ struct UserSessionStoreTests {
|
||||
|
||||
@Test
|
||||
func restoreSucceeds() async throws {
|
||||
// Given valid session data and a client factory that returns a client.
|
||||
// Given valid session data for Alice, stored after another account.
|
||||
let sessionDirectories = try makeValidSessionDirectories()
|
||||
defer { try? sessionDirectories.delete() }
|
||||
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: sessionDirectories)]
|
||||
keychainController.restorationTokensReturnValue = [makeCredentials(sessionDirectories: .init(), userID: "@bob:matrix.org"),
|
||||
makeCredentials(sessionDirectories: sessionDirectories)]
|
||||
|
||||
// When restoring the session.
|
||||
guard case .success(let userSession) = await store.restoreUserSession() else {
|
||||
// When restoring Alice's session.
|
||||
guard case .success(let userSession) = await store.restoreUserSession(userID: "@alice:matrix.org") else {
|
||||
Issue.record("Restoration should succeed.")
|
||||
return
|
||||
}
|
||||
|
||||
// Then a user session should be built for the restored client.
|
||||
// Then a user session should be built for Alice's client.
|
||||
#expect(userSession.clientProxy.userID == "@alice:matrix.org")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user