diff --git a/.github/workflows/triage-priority-bugs.yml b/.github/workflows/triage-priority-bugs.yml index 9e1eee8ba..548cd6aaa 100644 --- a/.github/workflows/triage-priority-bugs.yml +++ b/.github/workflows/triage-priority-bugs.yml @@ -25,7 +25,7 @@ jobs: - uses: alex-page/github-project-automation-plus@bb266ff4dde9242060e2d5418e120a133586d488 with: project: iOS App Team - column: P1 + column: "Important Issues & Topics (P1)" repo-token: ${{ secrets.ELEMENT_BOT_TOKEN }} P1_issues_to_crypto_team_workboard: diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index be8d11aad..4b69f1379 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -897,6 +897,7 @@ Tap the + to start adding people."; "manage_session_trusted" = "Trusted by you"; "manage_session_not_trusted" = "Not trusted"; "manage_session_sign_out" = "Sign out of this session"; +"manage_session_rename" = "Rename session"; // User sessions management "user_sessions_settings" = "Manage sessions"; @@ -1449,6 +1450,9 @@ Tap the + to start adding people."; // MARK: Sign out warning +"sign_out" = "Sign out"; +"sign_out_confirmation_message" = "Are you sure you want to sign out?"; + "sign_out_existing_key_backup_alert_title" = "Are you sure you want to sign out?"; "sign_out_existing_key_backup_alert_sign_out_action" = "Sign out"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ba12886a4..874702332 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3503,6 +3503,10 @@ public class VectorL10n: NSObject { public static var manageSessionNotTrusted: String { return VectorL10n.tr("Vector", "manage_session_not_trusted") } + /// Rename session + public static var manageSessionRename: String { + return VectorL10n.tr("Vector", "manage_session_rename") + } /// Sign out of this session public static var manageSessionSignOut: String { return VectorL10n.tr("Vector", "manage_session_sign_out") @@ -7756,6 +7760,14 @@ public class VectorL10n: NSObject { return VectorL10n.tr("Vector", "side_menu_reveal_action_accessibility_label") } /// Sign out + public static var signOut: String { + return VectorL10n.tr("Vector", "sign_out") + } + /// Are you sure you want to sign out? + public static var signOutConfirmationMessage: String { + return VectorL10n.tr("Vector", "sign_out_confirmation_message") + } + /// Sign out public static var signOutExistingKeyBackupAlertSignOutAction: String { return VectorL10n.tr("Vector", "sign_out_existing_key_backup_alert_sign_out_action") } diff --git a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift index 95efbdf9b..8fdfaeeb3 100644 --- a/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift +++ b/Riot/Modules/Settings/Discovery/ThreePidDetails/SettingsDiscoveryThreePidDetailsViewController.swift @@ -59,6 +59,7 @@ final class SettingsDiscoveryThreePidDetailsViewController: UIViewController { // Do any additional setup after loading the view. + vc_setLargeTitleDisplayMode(.never) self.setupViews() self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView) self.activityPresenter = ActivityIndicatorPresenter() diff --git a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift index 4b4d4d94e..3b5967717 100644 --- a/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift +++ b/Riot/Modules/Settings/IdentityServer/SettingsIdentityServerViewController.swift @@ -72,6 +72,7 @@ final class SettingsIdentityServerViewController: UIViewController { // Do any additional setup after loading the view. self.title = VectorL10n.identityServerSettingsTitle + vc_setLargeTitleDisplayMode(.never) self.setupViews() self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView) diff --git a/Riot/Modules/Settings/Language/LanguagePickerViewController.m b/Riot/Modules/Settings/Language/LanguagePickerViewController.m index 7c4c69055..1a7901efc 100644 --- a/Riot/Modules/Settings/Language/LanguagePickerViewController.m +++ b/Riot/Modules/Settings/Language/LanguagePickerViewController.m @@ -48,6 +48,8 @@ { [super viewDidLoad]; + [self vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; + // Hide line separators of empty cells self.tableView.tableFooterView = [[UIView alloc] init]; diff --git a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m index bdbc14ee1..be4e9110c 100644 --- a/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m +++ b/Riot/Modules/Settings/PhoneCountry/CountryPickerViewController.m @@ -50,6 +50,7 @@ // Hide line separators of empty cells self.tableView.tableFooterView = [[UIView alloc] init]; + [self vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; // Add a top view which will be displayed in case of vertical bounce. CGFloat height = self.tableView.frame.size.height; diff --git a/Riot/Modules/Settings/Security/SecurityViewController.m b/Riot/Modules/Settings/Security/SecurityViewController.m index 297e0b6eb..2e7993c38 100644 --- a/Riot/Modules/Settings/Security/SecurityViewController.m +++ b/Riot/Modules/Settings/Security/SecurityViewController.m @@ -156,6 +156,7 @@ TableViewSectionsDelegate> // Do any additional setup after loading the view, typically from a nib. self.navigationItem.title = [VectorL10n securitySettingsTitle]; + [self vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; [self vc_removeBackTitle]; [self.tableView registerClass:MXKTableViewCellWithLabelAndSwitch.class forCellReuseIdentifier:[MXKTableViewCellWithLabelAndSwitch defaultReuseIdentifier]]; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 1e1ec4d9f..893fc57e0 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2840,6 +2840,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationCopyrightUrlString]; webViewViewController.title = [VectorL10n settingsCopyright]; + [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; [self pushViewController:webViewViewController]; } @@ -2848,6 +2849,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationTermsConditionsUrlString]; webViewViewController.title = [VectorL10n settingsTermConditions]; + [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; [self pushViewController:webViewViewController]; } @@ -2856,6 +2858,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithURL:BuildSettings.applicationPrivacyPolicyUrlString]; webViewViewController.title = [VectorL10n settingsPrivacyPolicy]; + [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; [self pushViewController:webViewViewController]; } @@ -2866,6 +2869,7 @@ ChangePasswordCoordinatorBridgePresenterDelegate> WebViewViewController *webViewViewController = [[WebViewViewController alloc] initWithLocalHTMLFile:htmlFile]; webViewViewController.title = [VectorL10n settingsThirdPartyNotices]; + [webViewViewController vc_setLargeTitleDisplayMode:UINavigationItemLargeTitleDisplayModeNever]; [self pushViewController:webViewViewController]; } diff --git a/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift b/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift index dfa8c8ee5..f8827427a 100644 --- a/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift +++ b/Riot/Modules/UserInteractiveAuthentication/AuthenticatedEndpointRequest.swift @@ -29,3 +29,14 @@ class AuthenticatedEndpointRequest: NSObject { super.init() } } + +// MARK: - Helper methods + +extension AuthenticatedEndpointRequest { + /// Create an authenticated request on `_matrix/client/r0/devices/{deviceID}`. + /// - Parameter deviceID: The device ID that is to be deleted. + static func deleteDevice(_ deviceID: String) -> AuthenticatedEndpointRequest { + let path = String(format: "%@/devices/%@", kMXAPIPrefixPathR0, MXTools.encodeURIComponent(deviceID)) + return AuthenticatedEndpointRequest(path: path, httpMethod: "DELETE") + } +} diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift index 7ca59854f..330d7cedd 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift @@ -50,6 +50,7 @@ final class NotificationSettingsCoordinator: NotificationSettingsCoordinatorType } notificationSettingsViewModel = viewModel notificationSettingsViewController = viewController + notificationSettingsViewController.vc_setLargeTitleDisplayMode(.never) } // MARK: - Public methods diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 4f38ff3cd..32453a5de 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -15,15 +15,24 @@ // import CommonKit +import Foundation struct UserSessionsFlowCoordinatorParameters { let session: MXSession - let router: NavigationRouterType? + let router: NavigationRouterType } final class UserSessionsFlowCoordinator: Coordinator, Presentable { private let parameters: UserSessionsFlowCoordinatorParameters + private let navigationRouter: NavigationRouterType + private var reauthenticationPresenter: ReauthenticationCoordinatorBridgePresenter? + private var errorPresenter: MXKErrorPresentation + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + /// The root coordinator for user session management. + private weak var sessionsOverviewCoordinator: UserSessionsOverviewCoordinator? // Must be used only internally var childCoordinators: [Coordinator] = [] @@ -31,7 +40,10 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { init(parameters: UserSessionsFlowCoordinatorParameters) { self.parameters = parameters - navigationRouter = parameters.router ?? NavigationRouter(navigationController: RiotNavigationController()) + + self.navigationRouter = parameters.router + errorPresenter = MXKErrorAlertPresentation() + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.router.toPresentable()) } // MARK: - Private @@ -53,6 +65,10 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { coordinator.completion = { [weak self] result in guard let self = self else { return } switch result { + case let .renameSession(sessionInfo): + break + case let .logoutOfSession(sessionInfo): + self.showLogoutConfirmation(for: sessionInfo) case let .openSessionOverview(sessionInfo: sessionInfo): self.openSessionOverview(sessionInfo: sessionInfo) case let .openOtherSessions(sessionInfos: sessionInfos, filter: filter): @@ -81,6 +97,10 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { switch result { case let .openSessionDetails(sessionInfo: sessionInfo): self.openSessionDetails(sessionInfo: sessionInfo) + case let .renameSession(sessionInfo): + break + case let .logoutOfSession(sessionInfo): + self.showLogoutConfirmation(for: sessionInfo) } } pushScreen(with: coordinator) @@ -115,6 +135,89 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { return UserOtherSessionsCoordinator(parameters: parameters) } + + /// Shows a confirmation dialog to the user to sign out of a session. + private func showLogoutConfirmation(for sessionInfo: UserSessionInfo) { + // Use a UIAlertController as we don't have confirmationDialog in SwiftUI on iOS 14. + let alert = UIAlertController(title: VectorL10n.signOutConfirmationMessage, message: nil, preferredStyle: .actionSheet) + alert.addAction(UIAlertAction(title: VectorL10n.signOut, style: .destructive) { [weak self] _ in + self?.showLogoutAuthentication(for: sessionInfo) + }) + alert.addAction(UIAlertAction(title: VectorL10n.cancel, style: .cancel)) + alert.popoverPresentationController?.sourceView = toPresentable().view + + navigationRouter.present(alert, animated: true) + } + + /// Prompts the user to authenticate (if necessary) in order to log out of a specific session. + private func showLogoutAuthentication(for sessionInfo: UserSessionInfo) { + startLoading() + + let deleteDeviceRequest = AuthenticatedEndpointRequest.deleteDevice(sessionInfo.id) + let coordinatorParameters = ReauthenticationCoordinatorParameters(session: parameters.session, + presenter: navigationRouter.toPresentable(), + title: VectorL10n.deviceDetailsDeletePromptTitle, + message: VectorL10n.deviceDetailsDeletePromptMessage, + authenticatedEndpointRequest: deleteDeviceRequest) + let presenter = ReauthenticationCoordinatorBridgePresenter() + presenter.present(with: coordinatorParameters, animated: true) { [weak self] authenticationParameters in + self?.finalizeLogout(of: sessionInfo, with: authenticationParameters) + self?.reauthenticationPresenter = nil + } cancel: { [weak self] in + self?.stopLoading() + self?.reauthenticationPresenter = nil + } failure: { [weak self] error in + guard let self = self else { return } + self.stopLoading() + self.errorPresenter.presentError(from: self.toPresentable(), forError: error, animated: true, handler: { }) + self.reauthenticationPresenter = nil + } + + reauthenticationPresenter = presenter + } + + /// Finishes the logout process by deleting the device from the user's account. + /// - Parameters: + /// - sessionInfo: The `UserSessionInfo` for the session to be removed. + /// - authenticationParameters: The parameters from performing interactive authentication on the `devices` endpoint. + private func finalizeLogout(of sessionInfo: UserSessionInfo, with authenticationParameters: [String: Any]?) { + parameters.session.matrixRestClient.deleteDevice(sessionInfo.id, + authParameters: authenticationParameters ?? [:]) { [weak self] response in + guard let self = self else { return } + + self.stopLoading() + + guard response.isSuccess else { + MXLog.debug("[LogoutDeviceService] Delete device (\(sessionInfo.id) failed") + if let error = response.error { + self.errorPresenter.presentError(from: self.toPresentable(), forError: error, animated: true, handler: { }) + } else { + self.errorPresenter.presentGenericError(from: self.toPresentable(), animated: true, handler: { }) + } + + return + } + + self.popToSessionsOverview() + } + } + + /// Pops back to the root coordinator in the session management flow. + private func popToSessionsOverview() { + guard let sessionsOverviewCoordinator = sessionsOverviewCoordinator else { return } + navigationRouter.popToModule(sessionsOverviewCoordinator, animated: true) + } + + /// Show an activity indicator whilst loading. + private func startLoading() { + loadingIndicator = indicatorPresenter.present(.loading(label: VectorL10n.loading, isInteractionBlocking: true)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } + // MARK: - Public func start() { @@ -136,6 +239,8 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { self?.completion?() } } + + sessionsOverviewCoordinator = rootCoordinator } func toPresentable() -> UIViewController { diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift index a2b256177..498b9d391 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinatorBridgePresenter.swift @@ -42,12 +42,8 @@ final class UserSessionsFlowCoordinatorBridgePresenter: NSObject { // MARK: - Private - private func startUserSessionsFlow(mxSession: MXSession, navigationController: UINavigationController?) { - var navigationRouter: NavigationRouterType? - - if let navigationController = navigationController { - navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) - } + private func startUserSessionsFlow(mxSession: MXSession, navigationController: UINavigationController) { + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) let parameters = UserSessionsFlowCoordinatorParameters(session: mxSession, router: navigationRouter) let coordinator = UserSessionsFlowCoordinator(parameters: parameters) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift index cc6507213..de682c2d2 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Coordinator/UserSessionOverviewCoordinator.swift @@ -45,6 +45,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable { viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service) hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context)) + hostingController.vc_setLargeTitleDisplayMode(.never) + hostingController.vc_removeBackTitle() indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) } @@ -55,12 +57,17 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable { MXLog.debug("[UserSessionOverviewCoordinator] did start.") viewModel.completion = { [weak self] result in guard let self = self else { return } + MXLog.debug("[UserSessionOverviewCoordinator] UserSessionOverviewViewModel did complete with result: \(result).") switch result { case .verifyCurrentSession: break // TODO: case let .showSessionDetails(sessionInfo: sessionInfo): self.completion?(.openSessionDetails(sessionInfo: sessionInfo)) + case let .renameSession(sessionInfo): + self.completion?(.renameSession(sessionInfo)) + case let .logoutOfSession(sessionInfo): + self.completion?(.logoutOfSession(sessionInfo)) } } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift index 366da4f71..651e3386b 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewModels.swift @@ -20,6 +20,8 @@ import Foundation enum UserSessionOverviewCoordinatorResult { case openSessionDetails(sessionInfo: UserSessionInfo) + case renameSession(UserSessionInfo) + case logoutOfSession(UserSessionInfo) } // MARK: View model @@ -27,6 +29,8 @@ enum UserSessionOverviewCoordinatorResult { enum UserSessionOverviewViewModelResult: Equatable { case showSessionDetails(sessionInfo: UserSessionInfo) case verifyCurrentSession + case renameSession(UserSessionInfo) + case logoutOfSession(UserSessionInfo) } // MARK: View @@ -43,4 +47,6 @@ enum UserSessionOverviewViewAction { case verifyCurrentSession case viewSessionDetails case togglePushNotifications + case renameSession + case logoutOfSession } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift index a5c8f7d99..5596c703f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/UserSessionOverviewViewModel.swift @@ -69,6 +69,10 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio case .togglePushNotifications: self.state.showLoadingIndicator = true service.togglePushNotifications() + case .renameSession: + completion?(.renameSession(sessionInfo)) + case .logoutOfSession: + completion?(.logoutOfSession(sessionInfo)) } } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift index b2170f0d2..7b288ae4e 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverview.swift @@ -31,9 +31,11 @@ struct UserSessionOverview: View { }) .padding(16) SwiftUI.Section { - UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: { + UserSessionOverviewItem(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, + showsChevron: true) { viewModel.send(viewAction: .viewSessionDetails) - }) + } + if let enabled = viewModel.viewState.isPusherEnabled { UserSessionOverviewToggleCell(title: VectorL10n.userSessionPushNotifications, message: VectorL10n.userSessionPushNotificationsMessage, @@ -42,6 +44,13 @@ struct UserSessionOverview: View { } } } + + SwiftUI.Section { + UserSessionOverviewItem(title: VectorL10n.manageSessionSignOut, + isDestructive: true) { + viewModel.send(viewAction: .logoutOfSession) + } + } } .background(theme.colors.system.ignoresSafeArea()) .frame(maxHeight: .infinity) @@ -49,6 +58,19 @@ struct UserSessionOverview: View { .navigationTitle(viewModel.viewState.isCurrentSession ? VectorL10n.userSessionOverviewCurrentSessionTitle : VectorL10n.userSessionOverviewSessionTitle) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Menu { + Button { viewModel.send(viewAction: .renameSession) } label: { + Label(VectorL10n.manageSessionRename, systemImage: "pencil") + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + } + .accentColor(theme.colors.accent) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewDisclosureCell.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewItem.swift similarity index 59% rename from RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewDisclosureCell.swift rename to RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewItem.swift index 6fa8b00ca..de622b768 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewDisclosureCell.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/View/UserSessionOverviewItem.swift @@ -16,10 +16,12 @@ import SwiftUI -struct UserSessionOverviewDisclosureCell: View { +struct UserSessionOverviewItem: View { @Environment(\.theme) private var theme: ThemeSwiftUI let title: String + var showsChevron = false + var isDestructive = false var onBackgroundTap: (() -> Void)? var body: some View { @@ -29,9 +31,12 @@ struct UserSessionOverviewDisclosureCell: View { HStack { Text(title) .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) + .foregroundColor(textColor) .frame(maxWidth: .infinity, alignment: .leading) - Image(Asset.Images.chevron.name) + + if showsChevron { + Image(Asset.Images.chevron.name) + } } .padding(.vertical, 15) .padding(.horizontal, 16) @@ -40,17 +45,27 @@ struct UserSessionOverviewDisclosureCell: View { .background(theme.colors.background) } } + + var textColor: Color { + isDestructive ? theme.colors.alert : theme.colors.primaryContent + } } -struct UserSessionOverviewDisclosureCell_Previews: PreviewProvider { +struct UserSessionOverviewItem_Previews: PreviewProvider { + static var buttons: some View { + NavigationView { + ScrollView { + UserSessionOverviewItem(title: "Nav item", showsChevron: true) + UserSessionOverviewItem(title: "Button") + UserSessionOverviewItem(title: "Button", isDestructive: true) + } + } + } + static var previews: some View { Group { - UserSessionOverviewDisclosureCell(title: "Title") - .theme(.light) - .preferredColorScheme(.light) - UserSessionOverviewDisclosureCell(title: "Title") - .theme(.dark) - .preferredColorScheme(.dark) + buttons.theme(.light).preferredColorScheme(.light) + buttons.theme(.dark).preferredColorScheme(.dark) } } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index 981ffc212..30cc83767 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -40,7 +40,11 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { let dataProvider = UserSessionsDataProvider(session: parameters.session) service = UserSessionsOverviewService(dataProvider: dataProvider) viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) + hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context)) + hostingViewController.vc_setLargeTitleDisplayMode(.never) + hostingViewController.vc_removeBackTitle() + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController) } @@ -57,6 +61,10 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { self.showOtherSessions(sessionInfos: sessionInfos, filterBy: filter) case .verifyCurrentSession: self.startVerifyCurrentSession() + case .renameSession(let sessionInfo): + self.completion?(.renameSession(sessionInfo)) + case .logoutOfSession(let sessionInfo): + self.completion?(.logoutOfSession(sessionInfo)) case let .showCurrentSessionOverview(sessionInfo): self.showCurrentSessionOverview(sessionInfo: sessionInfo) case let .showUserSessionOverview(sessionInfo): diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index 7085a377a..601521000 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -19,6 +19,8 @@ import Foundation // MARK: - Coordinator enum UserSessionsOverviewCoordinatorResult { + case renameSession(UserSessionInfo) + case logoutOfSession(UserSessionInfo) case openSessionOverview(sessionInfo: UserSessionInfo) case openOtherSessions(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter) } @@ -28,6 +30,8 @@ enum UserSessionsOverviewCoordinatorResult { enum UserSessionsOverviewViewModelResult: Equatable { case showOtherSessions(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter) case verifyCurrentSession + case renameSession(UserSessionInfo) + case logoutOfSession(UserSessionInfo) case showCurrentSessionOverview(sessionInfo: UserSessionInfo) case showUserSessionOverview(sessionInfo: UserSessionInfo) } @@ -49,6 +53,8 @@ struct UserSessionsOverviewViewState: BindableState { enum UserSessionsOverviewViewAction { case viewAppeared case verifyCurrentSession + case renameCurrentSession + case logoutOfCurrentSession case viewCurrentSessionDetails case viewAllUnverifiedSessions case viewAllInactiveSessions diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index d52b29514..4e9cb90ea 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -39,6 +39,18 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess loadData() case .verifyCurrentSession: completion?(.verifyCurrentSession) + case .renameCurrentSession: + guard let currentSessionInfo = userSessionsOverviewService.overviewData.currentSession else { + assertionFailure("Missing current session") + return + } + completion?(.renameSession(currentSessionInfo)) + case .logoutOfCurrentSession: + guard let currentSessionInfo = userSessionsOverviewService.overviewData.currentSession else { + assertionFailure("Missing current session") + return + } + completion?(.logoutOfSession(currentSessionInfo)) case .viewCurrentSessionDetails: guard let currentSessionInfo = userSessionsOverviewService.overviewData.currentSession else { assertionFailure("Missing current session") diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 6823c9253..18f9ad91f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -36,7 +36,9 @@ struct UserSessionsOverview: View { .background(theme.colors.system.ignoresSafeArea()) .frame(maxHeight: .infinity) .navigationTitle(VectorL10n.userSessionsOverviewTitle) + .navigationBarTitleDisplayMode(.inline) .activityIndicator(show: viewModel.viewState.showLoadingIndicator) + .accentColor(theme.colors.accent) .onAppear { viewModel.send(viewAction: .viewAppeared) } @@ -91,18 +93,42 @@ struct UserSessionsOverview: View { viewModel.send(viewAction: .viewCurrentSessionDetails) }) } header: { - Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle) - .textCase(.uppercase) - .font(theme.fonts.footnote) - .foregroundColor(theme.colors.secondaryContent) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom, 12.0) - .padding(.top, 24.0) + HStack(alignment: .firstTextBaseline) { + Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle) + .textCase(.uppercase) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 12.0) + .padding(.top, 24.0) + + currentSessionMenu + } } .padding(.horizontal, 16) } } + private var currentSessionMenu: some View { + Menu { + Button { viewModel.send(viewAction: .renameCurrentSession) } label: { + Label(VectorL10n.manageSessionRename, systemImage: "pencil") + } + + if #available(iOS 15, *) { + Button(role: .destructive) { viewModel.send(viewAction: .logoutOfCurrentSession) } label: { + Label(VectorL10n.signOut, systemImage: "rectangle.portrait.and.arrow.right.fill") + } + } else { + Button { viewModel.send(viewAction: .logoutOfCurrentSession) } label: { + Label(VectorL10n.signOut, systemImage: "rectangle.righthalf.inset.fill.arrow.right") + } + } + } label: { + Image(systemName: "ellipsis.circle") + } + } + private var otherSessionsSection: some View { SwiftUI.Section { LazyVStack(spacing: 0) { diff --git a/changelog.d/6802.wip b/changelog.d/6802.wip new file mode 100644 index 000000000..ddc84c0cb --- /dev/null +++ b/changelog.d/6802.wip @@ -0,0 +1 @@ +Device Manager: Add logout actions to UserSessionsOverview and UserSessionOverview diff --git a/changelog.d/6804.bugfix b/changelog.d/6804.bugfix new file mode 100644 index 000000000..23cfc1ab2 --- /dev/null +++ b/changelog.d/6804.bugfix @@ -0,0 +1 @@ +Settings: Use regular titles for all of the sub-screens.