Session Manager: Single session logout

This commit is contained in:
Doug 2022-10-05 12:35:32 +01:00 committed by Doug
parent fea894ddcf
commit bb07a11e7e
13 changed files with 251 additions and 20 deletions

View file

@ -897,6 +897,7 @@ Tap the + to start adding people.";
"manage_session_trusted" = "Trusted by you"; "manage_session_trusted" = "Trusted by you";
"manage_session_not_trusted" = "Not trusted"; "manage_session_not_trusted" = "Not trusted";
"manage_session_sign_out" = "Sign out of this session"; "manage_session_sign_out" = "Sign out of this session";
"manage_session_rename" = "Rename session";
// User sessions management // User sessions management
"user_sessions_settings" = "Manage sessions"; "user_sessions_settings" = "Manage sessions";
@ -1449,6 +1450,9 @@ Tap the + to start adding people.";
// MARK: Sign out warning // 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_title" = "Are you sure you want to sign out?";
"sign_out_existing_key_backup_alert_sign_out_action" = "Sign out"; "sign_out_existing_key_backup_alert_sign_out_action" = "Sign out";

View file

@ -3503,6 +3503,10 @@ public class VectorL10n: NSObject {
public static var manageSessionNotTrusted: String { public static var manageSessionNotTrusted: String {
return VectorL10n.tr("Vector", "manage_session_not_trusted") 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 /// Sign out of this session
public static var manageSessionSignOut: String { public static var manageSessionSignOut: String {
return VectorL10n.tr("Vector", "manage_session_sign_out") 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") return VectorL10n.tr("Vector", "side_menu_reveal_action_accessibility_label")
} }
/// Sign out /// 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 { public static var signOutExistingKeyBackupAlertSignOutAction: String {
return VectorL10n.tr("Vector", "sign_out_existing_key_backup_alert_sign_out_action") return VectorL10n.tr("Vector", "sign_out_existing_key_backup_alert_sign_out_action")
} }

View file

@ -15,6 +15,7 @@
// //
import CommonKit import CommonKit
import Foundation
struct UserSessionsFlowCoordinatorParameters { struct UserSessionsFlowCoordinatorParameters {
let session: MXSession let session: MXSession
@ -23,7 +24,15 @@ struct UserSessionsFlowCoordinatorParameters {
final class UserSessionsFlowCoordinator: Coordinator, Presentable { final class UserSessionsFlowCoordinator: Coordinator, Presentable {
private let parameters: UserSessionsFlowCoordinatorParameters private let parameters: UserSessionsFlowCoordinatorParameters
private let navigationRouter: NavigationRouterType 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 // Must be used only internally
var childCoordinators: [Coordinator] = [] var childCoordinators: [Coordinator] = []
@ -31,7 +40,12 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
init(parameters: UserSessionsFlowCoordinatorParameters) { init(parameters: UserSessionsFlowCoordinatorParameters) {
self.parameters = parameters self.parameters = parameters
navigationRouter = parameters.router ?? NavigationRouter(navigationController: RiotNavigationController())
let navigationRouter = parameters.router ?? NavigationRouter(navigationController: RiotNavigationController())
self.navigationRouter = navigationRouter
errorPresenter = MXKErrorAlertPresentation()
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: navigationRouter.toPresentable())
} }
// MARK: - Private // MARK: - Private
@ -53,6 +67,10 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
coordinator.completion = { [weak self] result in coordinator.completion = { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
switch result { switch result {
case let .renameSession(sessionInfo):
break
case let .logoutOfSession(sessionInfo):
self.showLogoutConfirmation(for: sessionInfo)
case let .openSessionOverview(sessionInfo: sessionInfo): case let .openSessionOverview(sessionInfo: sessionInfo):
self.openSessionOverview(sessionInfo: sessionInfo) self.openSessionOverview(sessionInfo: sessionInfo)
case let .openOtherSessions(sessionsInfo: sessionsInfo, filter: filter): case let .openOtherSessions(sessionsInfo: sessionsInfo, filter: filter):
@ -81,6 +99,10 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
switch result { switch result {
case let .openSessionDetails(sessionInfo: sessionInfo): case let .openSessionDetails(sessionInfo: sessionInfo):
self.openSessionDetails(sessionInfo: sessionInfo) self.openSessionDetails(sessionInfo: sessionInfo)
case let .renameSession(sessionInfo):
break
case let .logoutOfSession(sessionInfo):
self.showLogoutConfirmation(for: sessionInfo)
} }
} }
pushScreen(with: coordinator) pushScreen(with: coordinator)
@ -115,6 +137,90 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
return UserOtherSessionsCoordinator(parameters: parameters) 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))
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 path = String(format: "%@/devices/%@", kMXAPIPrefixPathR0, MXTools.encodeURIComponent(sessionInfo.id))
let deleteDeviceRequest = AuthenticatedEndpointRequest(path: path, httpMethod: "DELETE")
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 // MARK: - Public
func start() { func start() {
@ -136,6 +242,8 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
self?.completion?() self?.completion?()
} }
} }
sessionsOverviewCoordinator = rootCoordinator
} }
func toPresentable() -> UIViewController { func toPresentable() -> UIViewController {

View file

@ -45,6 +45,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service) viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service)
hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context)) hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context))
hostingController.vc_setLargeTitleDisplayMode(.never)
hostingController.vc_removeBackTitle()
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingController)
} }
@ -55,12 +57,17 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
MXLog.debug("[UserSessionOverviewCoordinator] did start.") MXLog.debug("[UserSessionOverviewCoordinator] did start.")
viewModel.completion = { [weak self] result in viewModel.completion = { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
MXLog.debug("[UserSessionOverviewCoordinator] UserSessionOverviewViewModel did complete with result: \(result).") MXLog.debug("[UserSessionOverviewCoordinator] UserSessionOverviewViewModel did complete with result: \(result).")
switch result { switch result {
case .verifyCurrentSession: case .verifyCurrentSession:
break // TODO: break // TODO:
case let .showSessionDetails(sessionInfo: sessionInfo): case let .showSessionDetails(sessionInfo: sessionInfo):
self.completion?(.openSessionDetails(sessionInfo: sessionInfo)) self.completion?(.openSessionDetails(sessionInfo: sessionInfo))
case let .renameSession(sessionInfo):
self.completion?(.renameSession(sessionInfo))
case let .logoutOfSession(sessionInfo):
self.completion?(.logoutOfSession(sessionInfo))
} }
} }
} }

View file

@ -20,6 +20,8 @@ import Foundation
enum UserSessionOverviewCoordinatorResult { enum UserSessionOverviewCoordinatorResult {
case openSessionDetails(sessionInfo: UserSessionInfo) case openSessionDetails(sessionInfo: UserSessionInfo)
case renameSession(UserSessionInfo)
case logoutOfSession(UserSessionInfo)
} }
// MARK: View model // MARK: View model
@ -27,6 +29,8 @@ enum UserSessionOverviewCoordinatorResult {
enum UserSessionOverviewViewModelResult: Equatable { enum UserSessionOverviewViewModelResult: Equatable {
case showSessionDetails(sessionInfo: UserSessionInfo) case showSessionDetails(sessionInfo: UserSessionInfo)
case verifyCurrentSession case verifyCurrentSession
case renameSession(UserSessionInfo)
case logoutOfSession(UserSessionInfo)
} }
// MARK: View // MARK: View
@ -43,4 +47,6 @@ enum UserSessionOverviewViewAction {
case verifyCurrentSession case verifyCurrentSession
case viewSessionDetails case viewSessionDetails
case togglePushNotifications case togglePushNotifications
case renameSession
case logoutOfSession
} }

View file

@ -69,6 +69,10 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
case .togglePushNotifications: case .togglePushNotifications:
self.state.showLoadingIndicator = true self.state.showLoadingIndicator = true
service.togglePushNotifications() service.togglePushNotifications()
case .renameSession:
completion?(.renameSession(sessionInfo))
case .logoutOfSession:
completion?(.logoutOfSession(sessionInfo))
} }
} }
} }

View file

@ -31,9 +31,11 @@ struct UserSessionOverview: View {
}) })
.padding(16) .padding(16)
SwiftUI.Section { SwiftUI.Section {
UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: { UserSessionOverviewItem(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle,
showsChevron: true) {
viewModel.send(viewAction: .viewSessionDetails) viewModel.send(viewAction: .viewSessionDetails)
}) }
if let enabled = viewModel.viewState.isPusherEnabled { if let enabled = viewModel.viewState.isPusherEnabled {
UserSessionOverviewToggleCell(title: VectorL10n.userSessionPushNotifications, UserSessionOverviewToggleCell(title: VectorL10n.userSessionPushNotifications,
message: VectorL10n.userSessionPushNotificationsMessage, 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()) .background(theme.colors.system.ignoresSafeArea())
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
@ -49,6 +58,19 @@ struct UserSessionOverview: View {
.navigationTitle(viewModel.viewState.isCurrentSession ? .navigationTitle(viewModel.viewState.isCurrentSession ?
VectorL10n.userSessionOverviewCurrentSessionTitle : VectorL10n.userSessionOverviewCurrentSessionTitle :
VectorL10n.userSessionOverviewSessionTitle) 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)
} }
} }

View file

@ -16,10 +16,12 @@
import SwiftUI import SwiftUI
struct UserSessionOverviewDisclosureCell: View { struct UserSessionOverviewItem: View {
@Environment(\.theme) private var theme: ThemeSwiftUI @Environment(\.theme) private var theme: ThemeSwiftUI
let title: String let title: String
var showsChevron = false
var isDestructive = false
var onBackgroundTap: (() -> Void)? var onBackgroundTap: (() -> Void)?
var body: some View { var body: some View {
@ -29,9 +31,12 @@ struct UserSessionOverviewDisclosureCell: View {
HStack { HStack {
Text(title) Text(title)
.font(theme.fonts.body) .font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent) .foregroundColor(textColor)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
Image(Asset.Images.chevron.name)
if showsChevron {
Image(Asset.Images.chevron.name)
}
} }
.padding(.vertical, 15) .padding(.vertical, 15)
.padding(.horizontal, 16) .padding(.horizontal, 16)
@ -40,17 +45,27 @@ struct UserSessionOverviewDisclosureCell: View {
.background(theme.colors.background) .background(theme.colors.background)
} }
} }
var textColor: Color {
isDestructive ? theme.colors.alert : theme.colors.primaryContent
}
} }
struct UserSessionOverviewDisclosureCell_Previews: PreviewProvider { struct UserSessionOverviewButtonCell_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 { static var previews: some View {
Group { Group {
UserSessionOverviewDisclosureCell(title: "Title") buttons.theme(.light).preferredColorScheme(.light)
.theme(.light) buttons.theme(.dark).preferredColorScheme(.dark)
.preferredColorScheme(.light)
UserSessionOverviewDisclosureCell(title: "Title")
.theme(.dark)
.preferredColorScheme(.dark)
} }
} }
} }

View file

@ -40,7 +40,11 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
let dataProvider = UserSessionsDataProvider(session: parameters.session) let dataProvider = UserSessionsDataProvider(session: parameters.session)
service = UserSessionsOverviewService(dataProvider: dataProvider) service = UserSessionsOverviewService(dataProvider: dataProvider)
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service) viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context)) hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context))
hostingViewController.vc_setLargeTitleDisplayMode(.never)
hostingViewController.vc_removeBackTitle()
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController) indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController)
} }
@ -57,6 +61,10 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
self.showOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter) self.showOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter)
case .verifyCurrentSession: case .verifyCurrentSession:
self.startVerifyCurrentSession() self.startVerifyCurrentSession()
case .renameSession(let sessionInfo):
self.completion?(.renameSession(sessionInfo))
case .logoutOfSession(let sessionInfo):
self.completion?(.logoutOfSession(sessionInfo))
case let .showCurrentSessionOverview(sessionInfo): case let .showCurrentSessionOverview(sessionInfo):
self.showCurrentSessionOverview(sessionInfo: sessionInfo) self.showCurrentSessionOverview(sessionInfo: sessionInfo)
case let .showUserSessionOverview(sessionInfo): case let .showUserSessionOverview(sessionInfo):

View file

@ -19,6 +19,8 @@ import Foundation
// MARK: - Coordinator // MARK: - Coordinator
enum UserSessionsOverviewCoordinatorResult { enum UserSessionsOverviewCoordinatorResult {
case renameSession(UserSessionInfo)
case logoutOfSession(UserSessionInfo)
case openSessionOverview(sessionInfo: UserSessionInfo) case openSessionOverview(sessionInfo: UserSessionInfo)
case openOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter) case openOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
} }
@ -28,6 +30,8 @@ enum UserSessionsOverviewCoordinatorResult {
enum UserSessionsOverviewViewModelResult: Equatable { enum UserSessionsOverviewViewModelResult: Equatable {
case showOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter) case showOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
case verifyCurrentSession case verifyCurrentSession
case renameSession(UserSessionInfo)
case logoutOfSession(UserSessionInfo)
case showCurrentSessionOverview(sessionInfo: UserSessionInfo) case showCurrentSessionOverview(sessionInfo: UserSessionInfo)
case showUserSessionOverview(sessionInfo: UserSessionInfo) case showUserSessionOverview(sessionInfo: UserSessionInfo)
} }
@ -49,6 +53,8 @@ struct UserSessionsOverviewViewState: BindableState {
enum UserSessionsOverviewViewAction { enum UserSessionsOverviewViewAction {
case viewAppeared case viewAppeared
case verifyCurrentSession case verifyCurrentSession
case renameCurrentSession
case logoutOfCurrentSession
case viewCurrentSessionDetails case viewCurrentSessionDetails
case viewAllUnverifiedSessions case viewAllUnverifiedSessions
case viewAllInactiveSessions case viewAllInactiveSessions

View file

@ -39,6 +39,18 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
loadData() loadData()
case .verifyCurrentSession: case .verifyCurrentSession:
completion?(.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: case .viewCurrentSessionDetails:
guard let currentSessionInfo = userSessionsOverviewService.overviewData.currentSession else { guard let currentSessionInfo = userSessionsOverviewService.overviewData.currentSession else {
assertionFailure("Missing current session") assertionFailure("Missing current session")

View file

@ -36,7 +36,9 @@ struct UserSessionsOverview: View {
.background(theme.colors.system.ignoresSafeArea()) .background(theme.colors.system.ignoresSafeArea())
.frame(maxHeight: .infinity) .frame(maxHeight: .infinity)
.navigationTitle(VectorL10n.userSessionsOverviewTitle) .navigationTitle(VectorL10n.userSessionsOverviewTitle)
.navigationBarTitleDisplayMode(.inline)
.activityIndicator(show: viewModel.viewState.showLoadingIndicator) .activityIndicator(show: viewModel.viewState.showLoadingIndicator)
.accentColor(theme.colors.accent)
.onAppear { .onAppear {
viewModel.send(viewAction: .viewAppeared) viewModel.send(viewAction: .viewAppeared)
} }
@ -91,18 +93,42 @@ struct UserSessionsOverview: View {
viewModel.send(viewAction: .viewCurrentSessionDetails) viewModel.send(viewAction: .viewCurrentSessionDetails)
}) })
} header: { } header: {
Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle) HStack(alignment: .firstTextBaseline) {
.textCase(.uppercase) Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle)
.font(theme.fonts.footnote) .textCase(.uppercase)
.foregroundColor(theme.colors.secondaryContent) .font(theme.fonts.footnote)
.frame(maxWidth: .infinity, alignment: .leading) .foregroundColor(theme.colors.secondaryContent)
.padding(.bottom, 12.0) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 24.0) .padding(.bottom, 12.0)
.padding(.top, 24.0)
currentSessionMenu
}
} }
.padding(.horizontal, 16) .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 { private var otherSessionsSection: some View {
SwiftUI.Section { SwiftUI.Section {
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {

1
changelog.d/6802.wip Normal file
View file

@ -0,0 +1 @@
Device Manager: Add logout actions to UserSessionsOverview and UserSessionOverview