diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 5b693868ab..a3f65d00f6 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -56,6 +56,7 @@ 09AAF04B27732046C755D914 /* SoftLogoutViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32C5DAA1773F57653BF1C4F9 /* SoftLogoutViewModelTests.swift */; }; 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; }; 09D3D7D115318CAD131B4FE7 /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57084488B03BDB33C7B7CA0E /* ResolveVerifiedUserSendFailureScreenViewModelTests.swift */; }; + 0A0625A271EE5B06D2AAA069 /* HomeScreenSlidingSyncMigrationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4691B8DE1D51DE152680098A /* HomeScreenSlidingSyncMigrationBanner.swift */; }; 0A194F5E70B5A628C1BF4476 /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4999B5FD50AED7CB0F590FF8 /* AdvancedSettingsScreenModels.swift */; }; 0ACAA31FD0399CEEBA3ECC21 /* UserDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85149F56BA333619900E2410 /* UserDetailsEditScreenViewModelProtocol.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; @@ -1492,6 +1493,7 @@ 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = ""; }; 4629710C0337ADD9C8909542 /* ka */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ka; path = ka.lproj/Localizable.strings; sourceTree = ""; }; 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenModels.swift; sourceTree = ""; }; + 4691B8DE1D51DE152680098A /* HomeScreenSlidingSyncMigrationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenSlidingSyncMigrationBanner.swift; sourceTree = ""; }; 46A2AD86F7E618F468F6FAF5 /* VoiceMessageRecordingButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingButton.swift; sourceTree = ""; }; 46C208DA43CE25D13E670F40 /* UITestsAppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsAppCoordinator.swift; sourceTree = ""; }; 46D0BA44B1838E65B507B277 /* NotificationPermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationPermissionsScreen.swift; sourceTree = ""; }; @@ -3354,6 +3356,7 @@ 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */, ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */, C7661EFFCAA307A97D71132A /* HomeScreenRoomList.swift */, + 4691B8DE1D51DE152680098A /* HomeScreenSlidingSyncMigrationBanner.swift */, 84AF32E4136FD6F159D86C2C /* RoomDirectorySearchView.swift */, 037A5661B26EC6BE068188D7 /* Filters */, ); @@ -6372,6 +6375,7 @@ B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */, 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */, A10D6CCDE2010C09EEA1A593 /* HomeScreenRoomList.swift in Sources */, + 0A0625A271EE5B06D2AAA069 /* HomeScreenSlidingSyncMigrationBanner.swift in Sources */, DE4F8C4E0F1DB4832F09DE97 /* HomeScreenViewModel.swift in Sources */, 56F0A22972A3BB519DA2261C /* HomeScreenViewModelProtocol.swift in Sources */, 2BBE320EE426A347AAE5C7DA /* IdentityConfirmationScreen.swift in Sources */, diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 80f16478dd..02b0267fc6 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -407,12 +407,14 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { settingsFlowCoordinator.handleAppRoute(.chatBackupSettings, animated: true) case .presentStartChatScreen: stateMachine.processEvent(.showStartChatScreen) - case .logout: - Task { await self.runLogoutFlow() } case .presentGlobalSearch: presentGlobalSearch() case .presentRoomDirectorySearch: stateMachine.processEvent(.showRoomDirectorySearchScreen) + case .logoutWithoutConfirmation: + self.actionsSubject.send(.logout) + case .logout: + Task { await self.runLogoutFlow() } } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 7aec26f5d9..d9b7a2481f 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -24,6 +24,7 @@ enum HomeScreenCoordinatorAction { case presentStartChatScreen case presentGlobalSearch case presentRoomDirectorySearch + case logoutWithoutConfirmation case logout } @@ -64,14 +65,16 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentSettingsScreen) case .presentSecureBackupSettings: actionsSubject.send(.presentSecureBackupSettings) - case .logout: - actionsSubject.send(.logout) case .presentStartChatScreen: actionsSubject.send(.presentStartChatScreen) case .presentGlobalSearch: actionsSubject.send(.presentGlobalSearch) case .presentRoomDirectorySearch: actionsSubject.send(.presentRoomDirectorySearch) + case .logoutWithoutConfirmation: + actionsSubject.send(.logoutWithoutConfirmation) + case .logout: + actionsSubject.send(.logout) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index d855ffcdf3..0e2ee2bc37 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -19,6 +19,7 @@ enum HomeScreenViewModelAction { case presentStartChatScreen case presentGlobalSearch case presentRoomDirectorySearch + case logoutWithoutConfirmation case logout } @@ -31,6 +32,8 @@ enum HomeScreenViewAction { case startChat case confirmRecoveryKey case skipRecoveryKeyConfirmation + case confirmSlidingSyncUpgrade + case skipSlidingSyncUpgrade case updateVisibleItemRange(Range) case globalSearch case markRoomAsUnread(roomIdentifier: String) @@ -59,10 +62,10 @@ enum HomeScreenRoomListMode: CustomStringConvertible { } } -enum SecurityBannerMode { +enum HomeScreenBannerMode { case none case dismissed - case recoveryKeyConfirmation + case shown } struct HomeScreenViewState: BindableState { @@ -70,7 +73,9 @@ struct HomeScreenViewState: BindableState { var userDisplayName: String? var userAvatarURL: URL? - var securityBannerMode = SecurityBannerMode.none + var securityBannerMode = HomeScreenBannerMode.none + var slidingSyncMigrationBannerMode = HomeScreenBannerMode.none + var requiresExtraAccountSetup = false var rooms: [HomeScreenRoom] = [] @@ -110,10 +115,6 @@ struct HomeScreenViewState: BindableState { var shouldShowFilters: Bool { !bindings.isSearchFieldFocused && roomListMode == .rooms } - - var shouldShowRecoveryKeyConfirmationBanner: Bool { - securityBannerMode == .recoveryKeyConfirmation - } } struct HomeScreenViewStateBindings { diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 9c252f52ab..ab89e963ed 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -7,6 +7,7 @@ import AnalyticsEvents import Combine +import MatrixRustSDK import SwiftUI typealias HomeScreenViewModelType = StateStoreViewModel @@ -62,7 +63,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.requiresExtraAccountSetup = true if state.securityBannerMode != .dismissed { - state.securityBannerMode = .recoveryKeyConfirmation + state.securityBannerMode = .shown } default: state.securityBannerMode = .none @@ -117,6 +118,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol setupRoomListSubscriptions() updateRooms() + + Task { + await checkSlidingSyncMigration() + } } // MARK: - Public @@ -137,6 +142,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol actionsSubject.send(.presentSecureBackupSettings) case .skipRecoveryKeyConfirmation: state.securityBannerMode = .dismissed + case .confirmSlidingSyncUpgrade: + actionsSubject.send(.logout) + case .skipSlidingSyncUpgrade: + state.slidingSyncMigrationBannerMode = .dismissed case .updateVisibleItemRange(let range): roomSummaryProvider?.updateVisibleRange(range) case .startChat: @@ -192,12 +201,15 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol // perphery: ignore - used in release mode func presentCrashedLastRunAlert() { - state.bindings.alertInfo = AlertInfo(id: UUID(), - title: L10n.crashDetectionDialogContent(InfoPlistReader.main.bundleDisplayName), - primaryButton: .init(title: L10n.actionNo, action: nil), - secondaryButton: .init(title: L10n.actionYes) { [weak self] in - self?.actionsSubject.send(.presentFeedbackScreen) - }) + // Delay setting the alert otherwise it automatically gets dismissed. Same as the force logout one. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.state.bindings.alertInfo = AlertInfo(id: UUID(), + title: L10n.crashDetectionDialogContent(InfoPlistReader.main.bundleDisplayName), + primaryButton: .init(title: L10n.actionNo, action: nil), + secondaryButton: .init(title: L10n.actionYes) { [weak self] in + self?.actionsSubject.send(.presentFeedbackScreen) + }) + } } // MARK: - Private @@ -287,7 +299,40 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol state.rooms = rooms } + + /// Check whether we can inform the user about potential migrations + /// or have him logout as his proxy is no longer available + private func checkSlidingSyncMigration() async { + // Not logged in with a proxy, don't need to do anything + guard userSession.clientProxy.slidingSyncVersion.isProxy else { + return + } + + let versions = await userSession.clientProxy.availableSlidingSyncVersions + + // Native not available, nothing we can do + guard versions.contains(.native) else { + return + } + if versions.contains(.native) { + // Both available, prompt for migration + if versions.contains(where: \.isProxy) { + state.slidingSyncMigrationBannerMode = .shown + } else { // The proxy has been removed and logout is needed + // Delay setting the alert otherwise it automatically gets dismissed. Same as the crashed last run one + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.state.bindings.alertInfo = AlertInfo(id: UUID(), + title: L10n.bannerMigrateToNativeSlidingSyncForceLogoutTitle, + primaryButton: .init(title: L10n.bannerMigrateToNativeSlidingSyncAction, + action: { [weak self] in + self?.actionsSubject.send(.logoutWithoutConfirmation) + })) + } + } + } + } + private func markRoomAsFavourite(_ roomID: String, isFavourite: Bool) async { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { MXLog.error("Failed retrieving room for identifier: \(roomID)") @@ -412,3 +457,14 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol message: L10n.errorUnknown) } } + +extension SlidingSyncVersion { + var isProxy: Bool { + switch self { + case .proxy: + return true + default: + return false + } + } +} diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift index 541711e055..f64f84800f 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenContent.swift @@ -120,13 +120,16 @@ struct HomeScreenContent: View { private var topSection: some View { // An empty VStack causes glitches within the room list if context.viewState.shouldShowFilters || - context.viewState.shouldShowRecoveryKeyConfirmationBanner { + context.viewState.securityBannerMode == .shown || + context.viewState.slidingSyncMigrationBannerMode == .shown { VStack(spacing: 0) { if context.viewState.shouldShowFilters { RoomListFiltersView(state: $context.filtersState) } - - if context.viewState.shouldShowRecoveryKeyConfirmationBanner { + + if context.viewState.slidingSyncMigrationBannerMode == .shown { + HomeScreenSlidingSyncMigrationBanner(context: context) + } else if context.viewState.securityBannerMode == .shown { HomeScreenRecoveryKeyConfirmationBanner(context: context) } } diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenSlidingSyncMigrationBanner.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenSlidingSyncMigrationBanner.swift new file mode 100644 index 0000000000..ebb793ba63 --- /dev/null +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenSlidingSyncMigrationBanner.swift @@ -0,0 +1,68 @@ +// +// Copyright 2024 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only +// Please see LICENSE in the repository root for full details. +// + +import Combine +import SwiftUI + +struct HomeScreenSlidingSyncMigrationBanner: View { + @ObservedObject var context: HomeScreenViewModel.Context + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 16) { + Text(L10n.bannerMigrateToNativeSlidingSyncTitle) + .font(.compound.bodyLGSemibold) + .foregroundColor(.compound.textPrimary) + + Spacer() + + Button { + context.send(viewAction: .skipSlidingSyncUpgrade) + } label: { + Image(systemName: "xmark") + .foregroundColor(.compound.iconSecondary) + .frame(width: 12, height: 12) + } + } + Text(L10n.bannerMigrateToNativeSlidingSyncDescription) + .font(.compound.bodyMD) + .foregroundColor(.compound.textSecondary) + } + + Button(L10n.bannerMigrateToNativeSlidingSyncAction) { + context.send(viewAction: .confirmSlidingSyncUpgrade) + } + .frame(maxWidth: .infinity) + .buttonStyle(.compound(.primary, size: .medium)) + } + .padding(16) + .background(Color.compound.bgSubtleSecondary) + .cornerRadius(14) + .padding(.horizontal, 16) + } +} + +struct HomeScreenSlidingSyncMigrationBanner_Previews: PreviewProvider, TestablePreview { + static let viewModel = buildViewModel() + + static var previews: some View { + HomeScreenSlidingSyncMigrationBanner(context: viewModel.context) + } + + static func buildViewModel() -> HomeScreenViewModel { + let clientProxy = ClientProxyMock(.init()) + + let userSession = UserSessionMock(.init(clientProxy: clientProxy)) + + return HomeScreenViewModel(userSession: userSession, + analyticsService: ServiceLocator.shared.analytics, + appSettings: ServiceLocator.shared.settings, + selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), + userIndicatorController: ServiceLocator.shared.userIndicatorController) + } +}