mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-28 23:32:41 +00:00
Session Manager: Single session logout
This commit is contained in:
parent
fea894ddcf
commit
bb07a11e7e
13 changed files with 251 additions and 20 deletions
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,10 +31,13 @@ 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)
|
||||||
|
|
||||||
|
if showsChevron {
|
||||||
Image(Asset.Images.chevron.name)
|
Image(Asset.Images.chevron.name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
.padding(.vertical, 15)
|
.padding(.vertical, 15)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
SeparatorLine()
|
SeparatorLine()
|
||||||
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,6 +93,7 @@ struct UserSessionsOverview: View {
|
||||||
viewModel.send(viewAction: .viewCurrentSessionDetails)
|
viewModel.send(viewAction: .viewCurrentSessionDetails)
|
||||||
})
|
})
|
||||||
} header: {
|
} header: {
|
||||||
|
HStack(alignment: .firstTextBaseline) {
|
||||||
Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle)
|
Text(VectorL10n.userSessionsOverviewCurrentSessionSectionTitle)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
.font(theme.fonts.footnote)
|
.font(theme.fonts.footnote)
|
||||||
|
@ -98,11 +101,34 @@ struct UserSessionsOverview: View {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding(.bottom, 12.0)
|
.padding(.bottom, 12.0)
|
||||||
.padding(.top, 24.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
1
changelog.d/6802.wip
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Device Manager: Add logout actions to UserSessionsOverview and UserSessionOverview
|
Loading…
Reference in a new issue