diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index d56906240..955903d85 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -181,11 +181,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { // 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 - if sessionInfos.count == 1, let onlySession = sessionInfos.first { - self?.showLogoutAuthentication(for: onlySession) - } else { - self?.showLogoutAuthenticationAndLogoutFromSessions(sessionInfos: sessionInfos) - } + self?.showLogoutAuthentication(for: sessionInfos) }) alert.addAction(UIAlertAction(title: VectorL10n.cancel, style: .cancel)) alert.popoverPresentationController?.sourceView = toPresentable().view @@ -201,19 +197,20 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { signOutFlowPresenter = flowPresenter } - /// Prompts the user to authenticate (if necessary) in order to log out of a specific session. - private func showLogoutAuthentication(for sessionInfo: UserSessionInfo) { + /// Prompts the user to authenticate (if necessary) in order to log out of specific sessions. + private func showLogoutAuthentication(for sessionInfos: [UserSessionInfo]) { startLoading() - let deleteDeviceRequest = AuthenticatedEndpointRequest.deleteDevice(sessionInfo.id) + let deviceIDs = sessionInfos.map { $0.id } + let deleteDevicesRequest = AuthenticatedEndpointRequest.deleteDevices(deviceIDs) let coordinatorParameters = ReauthenticationCoordinatorParameters(session: parameters.session, presenter: navigationRouter.toPresentable(), title: VectorL10n.deviceDetailsDeletePromptTitle, message: VectorL10n.deviceDetailsDeletePromptMessage, - authenticatedEndpointRequest: deleteDeviceRequest) + authenticatedEndpointRequest: deleteDevicesRequest) let presenter = ReauthenticationCoordinatorBridgePresenter() presenter.present(with: coordinatorParameters, animated: true) { [weak self] authenticationParameters in - self?.finalizeLogout(of: sessionInfo, with: authenticationParameters) + self?.finalizeLogout(of: deviceIDs, with: authenticationParameters) self?.reauthenticationPresenter = nil } cancel: { [weak self] in self?.stopLoading() @@ -227,39 +224,13 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { reauthenticationPresenter = presenter } - - - - // TODO: move to into a command - private func showLogoutAuthenticationAndLogoutFromSessions(sessionInfos: [UserSessionInfo]) { - startLoading() - let deviceIds = sessionInfos.map { $0.id } - let deleteDeviceRequest = AuthenticatedEndpointRequest.deleteDevices(deviceIds) - 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?.finalizeLogout2(of: deviceIds, 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 - } - - private func finalizeLogout2(of deviceIds: [String], with authenticationParameters: [String: Any]?) { - - parameters.session.matrixRestClient.deleteDevices(deviceIds, + /// Finishes the logout process by deleting the devices from the user's account. + /// - Parameters: + /// - deviceIDs: IDs for the sessions to be removed. + /// - authenticationParameters: The parameters from performing interactive authentication on the `devices` endpoint. + private func finalizeLogout(of deviceIDs: [String], with authenticationParameters: [String: Any]?) { + parameters.session.matrixRestClient.deleteDevices(deviceIDs, authParameters: authenticationParameters ?? [:]) { [weak self] response in guard let self = self else { return } @@ -280,34 +251,6 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { } } - - - /// 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("[UserSessionsFlowCoordinator] 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() - } - } - private func showRenameSessionScreen(for sessionInfo: UserSessionInfo) { let parameters = UserSessionNameCoordinatorParameters(session: parameters.session, sessionInfo: sessionInfo) let coordinator = UserSessionNameCoordinator(parameters: parameters) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 9be3a08cb..891548af4 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -22,7 +22,7 @@ struct UserOtherSessions: View { @ObservedObject var viewModel: UserOtherSessionsViewModel.Context var body: some View { - VStack { + VStack(spacing: 0) { ScrollView { SwiftUI.Section { if viewModel.viewState.sessionItems.isEmpty { @@ -88,6 +88,7 @@ struct UserOtherSessions: View { LazyVStack(spacing: 0) { ForEach(viewModel.viewState.sessionItems) { viewData in UserSessionListItem(viewData: viewData, + isSeparatorHidden: viewData == viewModel.viewState.sessionItems.last, isEditModeEnabled: viewModel.isEditModeEnabled, onBackgroundTap: { sessionId in viewModel.send(viewAction: .userOtherSessionSelected(sessionId: sessionId)) }, onBackgroundLongPress: { _ in viewModel.isEditModeEnabled = true }) @@ -97,9 +98,8 @@ struct UserOtherSessions: View { } private func bottomToolbar() -> some View { - VStack{ + VStack (spacing: 0){ SeparatorLine() - .padding(0) HStack { Spacer() Button { diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 0705c8c54..66ad4245d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -17,19 +17,11 @@ import SwiftUI struct UserSessionListItem: View { - private enum LayoutConstants { - static let horizontalPadding: CGFloat = 15 - static let verticalPadding: CGFloat = 16 - static let avatarWidth: CGFloat = 40 - static let avatarRightMargin: CGFloat = 18 - } - @Environment(\.theme) private var theme: ThemeSwiftUI let viewData: UserSessionListItemViewData - + var isSeparatorHidden = false var isEditModeEnabled = false - var onBackgroundTap: ((String) -> Void)? var onBackgroundLongPress: ((String) -> Void)? @@ -42,38 +34,38 @@ struct UserSessionListItem: View { .frame(maxWidth: .infinity, alignment: .leading) .padding(4) } - VStack(alignment: .leading, spacing: LayoutConstants.verticalPadding) { - HStack(spacing: LayoutConstants.avatarRightMargin) { - if isEditModeEnabled { - Image(viewData.isSelected ? Asset.Images.userSessionListItemSelected.name : Asset.Images.userSessionListItemNotSelected.name) - } - DeviceAvatarView(viewData: viewData.deviceAvatarViewData, isSelected: viewData.isSelected) - VStack(alignment: .leading, spacing: 2) { - Text(viewData.sessionName) - .font(theme.fonts.bodySB) - .foregroundColor(theme.colors.primaryContent) - .multilineTextAlignment(.leading) - HStack { - if let sessionDetailsIcon = viewData.sessionDetailsIcon { - Image(sessionDetailsIcon) - .padding(.leading, 2) - } - Text(viewData.sessionDetails) - .font(theme.fonts.caption1) - .foregroundColor(viewData.highlightSessionDetails ? theme.colors.alert : theme.colors.secondaryContent) - .multilineTextAlignment(.leading) - } - } + HStack { + if isEditModeEnabled { + Image(viewData.isSelected ? Asset.Images.userSessionListItemSelected.name : Asset.Images.userSessionListItemNotSelected.name) } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, LayoutConstants.horizontalPadding) - - // Separator - // Note: Separator leading is matching the text leading, we could use alignment guide in the future - SeparatorLine() - .padding(.leading, LayoutConstants.horizontalPadding + LayoutConstants.avatarRightMargin + LayoutConstants.avatarWidth) + DeviceAvatarView(viewData: viewData.deviceAvatarViewData, isSelected: viewData.isSelected) + VStack(alignment: .leading, spacing: 0) { + Text(viewData.sessionName) + .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + .multilineTextAlignment(.leading) + .padding(.top, 16) + .padding(.bottom, 2) + .padding(.trailing, 16) + HStack { + if let sessionDetailsIcon = viewData.sessionDetailsIcon { + Image(sessionDetailsIcon) + .padding(.leading, 2) + } + Text(viewData.sessionDetails) + .font(theme.fonts.caption1) + .foregroundColor(viewData.highlightSessionDetails ? theme.colors.alert : theme.colors.secondaryContent) + .multilineTextAlignment(.leading) + } + .padding(.bottom, 16) + .padding(.trailing, 16) + SeparatorLine() + .isHidden(isSeparatorHidden) + } + .padding(.leading, 7) } - .padding(.top, LayoutConstants.verticalPadding) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 16) }.onTapGesture { onBackgroundTap?(viewData.sessionId) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift index 45d38ee79..5c75fde73 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionsOverview.swift @@ -151,7 +151,9 @@ struct UserSessionsOverview: View { SwiftUI.Section { LazyVStack(spacing: 0) { ForEach(viewModel.viewState.otherSessionsViewData.prefix(maxOtherSessionsToDisplay)) { viewData in - UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + UserSessionListItem(viewData: viewData, + isSeparatorHidden: viewData == viewModel.viewState.otherSessionsViewData.last, + onBackgroundTap: { sessionId in viewModel.send(viewAction: .tapUserSession(sessionId)) }) }