mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-22 01:22:46 +02:00
QR login from device manager (#6818)
* Add link device button into the sessions overview screen * Run Swift format * Fix tests * Fix a crash in tests * Fix PR remark
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -139,21 +139,21 @@ struct UserSessionCardViewPreview: View {
|
||||
|
||||
init(isCurrent: Bool = false) {
|
||||
let sessionInfo = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0",
|
||||
isActive: true,
|
||||
isCurrent: isCurrent)
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0",
|
||||
isActive: true,
|
||||
isCurrent: isCurrent)
|
||||
viewData = UserSessionCardViewData(sessionInfo: sessionInfo)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
||||
init(parameters: UserSessionsFlowCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
self.navigationRouter = parameters.router
|
||||
navigationRouter = parameters.router
|
||||
errorPresenter = MXKErrorAlertPresentation()
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: parameters.router.toPresentable())
|
||||
}
|
||||
@@ -75,6 +75,8 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
||||
self.openOtherSessions(sessionInfos: sessionInfos,
|
||||
filterBy: filter,
|
||||
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||
case .linkDevice:
|
||||
self.openQRLoginScreen()
|
||||
}
|
||||
}
|
||||
return coordinator
|
||||
@@ -105,6 +107,21 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
/// Shows the QR login screen.
|
||||
private func openQRLoginScreen() {
|
||||
let service = QRLoginService(client: parameters.session.matrixRestClient,
|
||||
mode: .authenticated)
|
||||
let parameters = AuthenticationQRLoginStartCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
qrLoginService: service)
|
||||
let coordinator = AuthenticationQRLoginStartCoordinator(parameters: parameters)
|
||||
coordinator.callback = { [weak self, weak coordinator] _ in
|
||||
guard let self = self, let coordinator = coordinator else { return }
|
||||
self.remove(childCoordinator: coordinator)
|
||||
}
|
||||
|
||||
pushScreen(with: coordinator)
|
||||
}
|
||||
|
||||
private func createUserSessionOverviewCoordinator(sessionInfo: UserSessionInfo) -> UserSessionOverviewCoordinator {
|
||||
let parameters = UserSessionOverviewCoordinatorParameters(session: parameters.session,
|
||||
@@ -135,7 +152,6 @@ 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.
|
||||
|
||||
-1
@@ -24,7 +24,6 @@ struct UserOtherSessionsCoordinatorParameters {
|
||||
}
|
||||
|
||||
final class UserOtherSessionsCoordinator: Coordinator, Presentable {
|
||||
|
||||
private let parameters: UserOtherSessionsCoordinatorParameters
|
||||
private let userOtherSessionsHostingController: UIViewController
|
||||
private var userOtherSessionsViewModel: UserOtherSessionsViewModelProtocol
|
||||
|
||||
+5
-7
@@ -40,7 +40,6 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
|
||||
let viewModel: UserOtherSessionsViewModel
|
||||
switch self {
|
||||
case .inactiveSessions:
|
||||
@@ -83,7 +82,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8_000_000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
@@ -99,7 +98,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
deviceType: .web,
|
||||
isVerified: true,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 9000000,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 9_000_000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
@@ -115,7 +114,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10000000,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10_000_000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
@@ -150,7 +149,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
deviceType: .desktop,
|
||||
isVerified: false,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8_000_000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
@@ -160,7 +159,6 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
clientName: nil,
|
||||
clientVersion: nil,
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
]
|
||||
isCurrent: false)]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -18,12 +18,11 @@ import RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
class UserOtherSessionsUITests: MockScreenTestCase {
|
||||
|
||||
func test_whenOtherSessionsWithInactiveSessionFilterPresented_correctHeaderDisplayed() {
|
||||
app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.inactiveSessions.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo].exists)
|
||||
}
|
||||
|
||||
func test_whenOtherSessionsWithInactiveSessionFilterPresented_correctItemsDisplayed() {
|
||||
|
||||
+5
-8
@@ -19,14 +19,12 @@ import XCTest
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserOtherSessionsViewModelTests: XCTestCase {
|
||||
|
||||
|
||||
func test_whenUserOtherSessionSelectedProcessed_completionWithShowUserSessionOverviewCalled() {
|
||||
let expectedUserSessionInfo = createUserSessionInfo(sessionId: "session 2")
|
||||
let sut = UserOtherSessionsViewModel(sessionInfos: [createUserSessionInfo(sessionId: "session 1"),
|
||||
expectedUserSessionInfo],
|
||||
filter: .inactive,
|
||||
title: "Title")
|
||||
expectedUserSessionInfo],
|
||||
filter: .inactive,
|
||||
title: "Title")
|
||||
|
||||
var modelResult: UserOtherSessionsViewModelResult?
|
||||
sut.completion = { result in
|
||||
@@ -39,8 +37,8 @@ class UserOtherSessionsViewModelTests: XCTestCase {
|
||||
func test_whenModelCreated_withInactiveFilter_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"), createUserSessionInfo(sessionId: "session 2")]
|
||||
let sut = UserOtherSessionsViewModel(sessionInfos: sessionInfos,
|
||||
filter: .inactive,
|
||||
title: "Title")
|
||||
filter: .inactive,
|
||||
title: "Title")
|
||||
|
||||
let expectedHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
|
||||
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
|
||||
@@ -51,7 +49,6 @@ class UserOtherSessionsViewModelTests: XCTestCase {
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
|
||||
private func createUserSessionInfo(sessionId: String) -> UserSessionInfo {
|
||||
UserSessionInfo(id: sessionId,
|
||||
name: "iOS",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Coordinator
|
||||
|
||||
enum UserOtherSessionsCoordinatorResult {
|
||||
case openSessionDetails(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
@@ -38,6 +39,7 @@ enum UserOtherSessionsSection: Hashable, Identifiable {
|
||||
var id: Self {
|
||||
self
|
||||
}
|
||||
|
||||
case sessionItems(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData])
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ enum OtherUserSessionsFilter {
|
||||
}
|
||||
|
||||
class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol {
|
||||
|
||||
var completion: ((UserOtherSessionsViewModelResult) -> Void)?
|
||||
private let sessionInfos: [UserSessionInfo]
|
||||
|
||||
@@ -42,7 +41,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
|
||||
override func process(viewAction: UserOtherSessionsViewAction) {
|
||||
switch viewAction {
|
||||
case let .userOtherSessionSelected(sessionId: sessionId):
|
||||
guard let session = sessionInfos.first(where: {$0.id == sessionId}) else {
|
||||
guard let session = sessionInfos.first(where: { $0.id == sessionId }) else {
|
||||
assertionFailure("Session should exist in the array.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import SwiftUI
|
||||
|
||||
struct UserOtherSessions: View {
|
||||
|
||||
@Environment(\.theme) private var theme
|
||||
|
||||
@ObservedObject var viewModel: UserOtherSessionsViewModel.Context
|
||||
@@ -57,7 +56,6 @@ struct UserOtherSessions: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct UserOtherSessions_Previews: PreviewProvider {
|
||||
|
||||
static let stateRenderer = MockUserOtherSessionsScreenState.stateRenderer
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
+1
-4
@@ -23,7 +23,6 @@ struct UserOtherSessionsHeaderViewData: Hashable {
|
||||
}
|
||||
|
||||
struct UserOtherSessionsHeaderView: View {
|
||||
|
||||
private var backgroundShape: RoundedRectangle {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
}
|
||||
@@ -33,7 +32,7 @@ struct UserOtherSessionsHeaderView: View {
|
||||
let viewData: UserOtherSessionsHeaderViewData
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top, spacing: 0) {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
if let iconName = viewData.iconName {
|
||||
Image(iconName)
|
||||
.frame(width: 40, height: 40)
|
||||
@@ -63,12 +62,10 @@ struct UserOtherSessionsHeaderView: View {
|
||||
// MARK: - Previews
|
||||
|
||||
struct UserOtherSessionsHeaderView_Previews: PreviewProvider {
|
||||
|
||||
private static let inactiveSessionViewData = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
|
||||
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
|
||||
iconName: Asset.Images.userOtherSessionsInactive.name)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
UserOtherSessionsHeaderView(viewData: self.inactiveSessionViewData)
|
||||
|
||||
+30
-30
@@ -42,38 +42,38 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
|
||||
switch self {
|
||||
case .allSections:
|
||||
sessionInfo = UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
case .sessionSectionOnly:
|
||||
sessionInfo = UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
name: "Android",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: "Element",
|
||||
clientVersion: "1.0.0",
|
||||
isActive: true,
|
||||
isCurrent: false)
|
||||
}
|
||||
let viewModel = UserSessionDetailsViewModel(sessionInfo: sessionInfo)
|
||||
|
||||
|
||||
+14
-15
@@ -18,7 +18,6 @@ import Combine
|
||||
import MatrixSDK
|
||||
|
||||
class UserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
|
||||
// MARK: - Members
|
||||
|
||||
private(set) var pusherEnabledSubject: CurrentValueSubject<Bool?, Never>
|
||||
@@ -36,10 +35,10 @@ class UserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
init(session: MXSession, sessionInfo: UserSessionInfo) {
|
||||
self.session = session
|
||||
self.sessionInfo = sessionInfo
|
||||
self.pusherEnabledSubject = CurrentValueSubject(nil)
|
||||
self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(false)
|
||||
pusherEnabledSubject = CurrentValueSubject(nil)
|
||||
remotelyTogglingPushersAvailableSubject = CurrentValueSubject(false)
|
||||
|
||||
self.localNotificationSettings = session.accountData.localNotificationSettingsForDevice(withId: sessionInfo.id)
|
||||
localNotificationSettings = session.accountData.localNotificationSettingsForDevice(withId: sessionInfo.id)
|
||||
|
||||
if let localNotificationSettings = localNotificationSettings, let isSilenced = localNotificationSettings[kMXAccountDataIsSilencedKey] as? Bool {
|
||||
remotelyTogglingPushersAvailableSubject.send(true)
|
||||
@@ -69,7 +68,7 @@ class UserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
private func toggle(_ pusher: MXPusher, enabled: Bool) {
|
||||
guard self.remotelyTogglingPushersAvailableSubject.value else {
|
||||
guard remotelyTogglingPushersAvailableSubject.value else {
|
||||
MXLog.warning("[UserSessionOverviewService] toggle pusher canceled: remotely toggling pushers not available")
|
||||
return
|
||||
}
|
||||
@@ -77,16 +76,16 @@ class UserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
MXLog.debug("[UserSessionOverviewService] remotely toggling pusher")
|
||||
let data = pusher.data.jsonDictionary() as? [String: Any] ?? [:]
|
||||
|
||||
self.session.matrixRestClient.setPusher(pushKey: pusher.pushkey,
|
||||
kind: MXPusherKind(value: pusher.kind),
|
||||
appId: pusher.appId,
|
||||
appDisplayName:pusher.appDisplayName,
|
||||
deviceDisplayName: pusher.deviceDisplayName,
|
||||
profileTag: pusher.profileTag ?? "",
|
||||
lang: pusher.lang,
|
||||
data: data,
|
||||
append: false,
|
||||
enabled: enabled) { [weak self] response in
|
||||
session.matrixRestClient.setPusher(pushKey: pusher.pushkey,
|
||||
kind: MXPusherKind(value: pusher.kind),
|
||||
appId: pusher.appId,
|
||||
appDisplayName: pusher.appDisplayName,
|
||||
deviceDisplayName: pusher.deviceDisplayName,
|
||||
profileTag: pusher.profileTag ?? "",
|
||||
lang: pusher.lang,
|
||||
data: data,
|
||||
append: false,
|
||||
enabled: enabled) { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
|
||||
switch response {
|
||||
|
||||
+3
-5
@@ -18,18 +18,16 @@ import Combine
|
||||
import Foundation
|
||||
|
||||
class MockUserSessionOverviewService: UserSessionOverviewServiceProtocol {
|
||||
|
||||
|
||||
var pusherEnabledSubject: CurrentValueSubject<Bool?, Never>
|
||||
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
|
||||
|
||||
init(pusherEnabled: Bool? = nil, remotelyTogglingPushersAvailable: Bool = true) {
|
||||
self.pusherEnabledSubject = CurrentValueSubject(pusherEnabled)
|
||||
self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(remotelyTogglingPushersAvailable)
|
||||
pusherEnabledSubject = CurrentValueSubject(pusherEnabled)
|
||||
remotelyTogglingPushersAvailableSubject = CurrentValueSubject(remotelyTogglingPushersAvailable)
|
||||
}
|
||||
|
||||
func togglePushNotifications() {
|
||||
guard let enabled = pusherEnabledSubject.value, self.remotelyTogglingPushersAvailableSubject.value else {
|
||||
guard let enabled = pusherEnabledSubject.value, remotelyTogglingPushersAvailableSubject.value else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -20,7 +20,6 @@ import XCTest
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserSessionOverviewViewModelTests: XCTestCase {
|
||||
|
||||
func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() {
|
||||
let sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService())
|
||||
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
|
||||
case .viewSessionDetails:
|
||||
completion?(.showSessionDetails(sessionInfo: sessionInfo))
|
||||
case .togglePushNotifications:
|
||||
self.state.showLoadingIndicator = true
|
||||
state.showLoadingIndicator = true
|
||||
service.togglePushNotifications()
|
||||
case .renameSession:
|
||||
completion?(.renameSession(sessionInfo))
|
||||
|
||||
+2
-1
@@ -69,6 +69,8 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
self.showCurrentSessionOverview(sessionInfo: sessionInfo)
|
||||
case let .showUserSessionOverview(sessionInfo):
|
||||
self.showUserSessionOverview(sessionInfo: sessionInfo)
|
||||
case .linkDevice:
|
||||
self.completion?(.linkDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,5 +109,4 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
private func showUserSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
@@ -47,4 +47,10 @@ class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
||||
func accountData(for eventType: String) -> [AnyHashable: Any]? {
|
||||
session.accountData.accountData(forEventType: eventType)
|
||||
}
|
||||
|
||||
func qrLoginAvailable() async throws -> Bool {
|
||||
let service = QRLoginService(client: session.matrixRestClient,
|
||||
mode: .authenticated)
|
||||
return try await service.isServiceAvailable()
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -29,4 +29,6 @@ protocol UserSessionsDataProviderProtocol {
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo?
|
||||
|
||||
func accountData(for eventType: String) -> [AnyHashable: Any]?
|
||||
|
||||
func qrLoginAvailable() async throws -> Bool
|
||||
}
|
||||
|
||||
+15
-7
@@ -32,7 +32,8 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false)
|
||||
sessionInfos = []
|
||||
setupInitialOverviewData()
|
||||
}
|
||||
@@ -44,8 +45,12 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
switch response {
|
||||
case .success(let devices):
|
||||
self.sessionInfos = self.sortedSessionInfos(from: devices)
|
||||
self.overviewData = self.sessionsOverviewData(from: self.sessionInfos)
|
||||
completion(.success(self.overviewData))
|
||||
Task { @MainActor in
|
||||
let linkDeviceEnabled = try? await self.dataProvider.qrLoginAvailable()
|
||||
self.overviewData = self.sessionsOverviewData(from: self.sessionInfos,
|
||||
linkDeviceEnabled: linkDeviceEnabled ?? false)
|
||||
completion(.success(self.overviewData))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
@@ -59,7 +64,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
|
||||
return overviewData.otherSessions.first(where: { $0.id == sessionId })
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func setupInitialOverviewData() {
|
||||
@@ -70,7 +75,8 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
|
||||
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||
otherSessions: [])
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false)
|
||||
}
|
||||
|
||||
private func getCurrentSessionInfo() -> UserSessionInfo? {
|
||||
@@ -87,11 +93,13 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) }
|
||||
}
|
||||
|
||||
private func sessionsOverviewData(from allSessions: [UserSessionInfo]) -> UserSessionsOverviewData {
|
||||
private func sessionsOverviewData(from allSessions: [UserSessionInfo],
|
||||
linkDeviceEnabled: Bool) -> UserSessionsOverviewData {
|
||||
UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
||||
inactiveSessions: allSessions.filter { !$0.isActive },
|
||||
otherSessions: allSessions.filter { !$0.isCurrent })
|
||||
otherSessions: allSessions.filter { !$0.isCurrent },
|
||||
linkDeviceEnabled: linkDeviceEnabled)
|
||||
}
|
||||
|
||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||
|
||||
+12
-8
@@ -17,7 +17,6 @@
|
||||
import Foundation
|
||||
|
||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
|
||||
enum Mode {
|
||||
case currentSessionUnverified
|
||||
case currentSessionVerified
|
||||
@@ -37,7 +36,8 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false)
|
||||
}
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
@@ -49,24 +49,28 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
otherSessions: [],
|
||||
linkDeviceEnabled: false)
|
||||
case .onlyUnverifiedSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: unverifiedSessions + [currentSession],
|
||||
inactiveSessions: [],
|
||||
otherSessions: unverifiedSessions)
|
||||
otherSessions: unverifiedSessions,
|
||||
linkDeviceEnabled: false)
|
||||
case .onlyInactiveSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: inactiveSessions)
|
||||
otherSessions: inactiveSessions,
|
||||
linkDeviceEnabled: false)
|
||||
default:
|
||||
let otherSessions = unverifiedSessions + inactiveSessions + buildSessions(verified: true, active: true)
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: unverifiedSessions,
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: otherSessions)
|
||||
otherSessions: otherSessions,
|
||||
linkDeviceEnabled: true)
|
||||
}
|
||||
|
||||
completion(.success(overviewData))
|
||||
@@ -75,7 +79,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
||||
overviewData.otherSessions.first { $0.id == sessionId }
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var currentSession: UserSessionInfo {
|
||||
@@ -103,7 +107,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
deviceType: .desktop,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8_000_000,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ struct UserSessionsOverviewData {
|
||||
let unverifiedSessions: [UserSessionInfo]
|
||||
let inactiveSessions: [UserSessionInfo]
|
||||
let otherSessions: [UserSessionInfo]
|
||||
let linkDeviceEnabled: Bool
|
||||
}
|
||||
|
||||
protocol UserSessionsOverviewServiceProtocol {
|
||||
|
||||
+21
@@ -23,6 +23,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||
|
||||
XCTAssertTrue(app.buttons["userSessionCardVerifyButton"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
|
||||
|
||||
verifyLinkDeviceButtonStatus(true)
|
||||
}
|
||||
|
||||
func testCurrentSessionVerified() {
|
||||
@@ -30,6 +32,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||
|
||||
XCTAssertFalse(app.buttons["userSessionCardVerifyButton"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
|
||||
|
||||
verifyLinkDeviceButtonStatus(true)
|
||||
}
|
||||
|
||||
func testOnlyUnverifiedSessions() {
|
||||
@@ -37,6 +41,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||
|
||||
verifyLinkDeviceButtonStatus(false)
|
||||
}
|
||||
|
||||
func testOnlyInactiveSessions() {
|
||||
@@ -44,6 +50,8 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||
|
||||
verifyLinkDeviceButtonStatus(false)
|
||||
}
|
||||
|
||||
func testNoOtherSessions() {
|
||||
@@ -51,5 +59,18 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||
|
||||
XCTAssertFalse(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||
XCTAssertFalse(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||
|
||||
verifyLinkDeviceButtonStatus(false)
|
||||
}
|
||||
|
||||
func verifyLinkDeviceButtonStatus(_ enabled: Bool) {
|
||||
if enabled {
|
||||
let linkDeviceButton = app.buttons["linkDeviceButton"]
|
||||
XCTAssertTrue(linkDeviceButton.exists)
|
||||
XCTAssertTrue(linkDeviceButton.isEnabled)
|
||||
} else {
|
||||
let linkDeviceButton = app.buttons["linkDeviceButton"]
|
||||
XCTAssertFalse(linkDeviceButton.exists)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -27,6 +27,7 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||
XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||
XCTAssertTrue(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||
XCTAssertTrue(viewModel.state.otherSessionsViewData.isEmpty)
|
||||
XCTAssertFalse(viewModel.state.linkDeviceButtonVisible)
|
||||
}
|
||||
|
||||
func testLoadOnDidAppear() {
|
||||
@@ -37,6 +38,7 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||
XCTAssertFalse(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||
XCTAssertFalse(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||
XCTAssertFalse(viewModel.state.otherSessionsViewData.isEmpty)
|
||||
XCTAssertTrue(viewModel.state.linkDeviceButtonVisible)
|
||||
}
|
||||
|
||||
func testSimpleActionProcessing() {
|
||||
@@ -52,6 +54,9 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||
|
||||
viewModel.process(viewAction: .viewAllInactiveSessions)
|
||||
XCTAssertEqual(result, .showOtherSessions(sessionInfos: [], filter: .inactive))
|
||||
|
||||
viewModel.process(viewAction: .linkDevice)
|
||||
XCTAssertEqual(result, .linkDevice)
|
||||
}
|
||||
|
||||
func testShowSessionDetails() {
|
||||
|
||||
@@ -23,6 +23,7 @@ enum UserSessionsOverviewCoordinatorResult {
|
||||
case logoutOfSession(UserSessionInfo)
|
||||
case openSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case openOtherSessions(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case linkDevice
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
@@ -34,6 +35,7 @@ enum UserSessionsOverviewViewModelResult: Equatable {
|
||||
case logoutOfSession(UserSessionInfo)
|
||||
case showCurrentSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case showUserSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case linkDevice
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
@@ -48,6 +50,8 @@ struct UserSessionsOverviewViewState: BindableState {
|
||||
var otherSessionsViewData = [UserSessionListItemViewData]()
|
||||
|
||||
var showLoadingIndicator = false
|
||||
|
||||
var linkDeviceButtonVisible = false
|
||||
}
|
||||
|
||||
enum UserSessionsOverviewViewAction {
|
||||
@@ -60,4 +64,5 @@ enum UserSessionsOverviewViewAction {
|
||||
case viewAllInactiveSessions
|
||||
case viewAllOtherSessions
|
||||
case tapUserSession(_ sessionId: String)
|
||||
case linkDevice
|
||||
}
|
||||
|
||||
+4
-1
@@ -70,6 +70,8 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
return
|
||||
}
|
||||
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||
case .linkDevice:
|
||||
completion?(.linkDevice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +85,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
if let currentSessionInfo = userSessionsViewData.currentSession {
|
||||
state.currentSessionViewData = UserSessionCardViewData(sessionInfo: currentSessionInfo)
|
||||
}
|
||||
state.linkDeviceButtonVisible = userSessionsViewData.linkDeviceEnabled
|
||||
}
|
||||
|
||||
private func loadData() {
|
||||
@@ -113,6 +116,6 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
|
||||
extension Collection where Element == UserSessionInfo {
|
||||
func asViewData() -> [UserSessionListItemViewData] {
|
||||
map { UserSessionListItemViewDataFactory().create(from: $0)}
|
||||
map { UserSessionListItemViewDataFactory().create(from: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -18,7 +18,6 @@ import Foundation
|
||||
|
||||
/// View data for UserSessionListItem
|
||||
struct UserSessionListItemViewData: Identifiable, Hashable {
|
||||
|
||||
var id: String {
|
||||
sessionId
|
||||
}
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@
|
||||
import Foundation
|
||||
|
||||
struct UserSessionListItemViewDataFactory {
|
||||
|
||||
func create(from sessionInfo: UserSessionInfo, highlightSessionDetails: Bool = false) -> UserSessionListItemViewData {
|
||||
let sessionName = UserSessionNameFormatter.sessionName(deviceType: sessionInfo.deviceType,
|
||||
sessionDisplayName: sessionInfo.name)
|
||||
@@ -41,7 +40,7 @@ struct UserSessionListItemViewDataFactory {
|
||||
}
|
||||
|
||||
private func inactiveSessionDetails(sessionInfo: UserSessionInfo) -> String {
|
||||
if let lastActivityDate = sessionInfo.lastSeenTimestamp {
|
||||
if let lastActivityDate = sessionInfo.lastSeenTimestamp {
|
||||
let lastActivityDateString = InactiveUserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString)
|
||||
}
|
||||
|
||||
+36
-9
@@ -22,15 +22,25 @@ struct UserSessionsOverview: View {
|
||||
@ObservedObject var viewModel: UserSessionsOverviewViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
if hasSecurityRecommendations {
|
||||
securityRecommendationsSection
|
||||
}
|
||||
|
||||
currentSessionsSection
|
||||
|
||||
if !viewModel.viewState.otherSessionsViewData.isEmpty {
|
||||
otherSessionsSection
|
||||
GeometryReader { geometry in
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
ScrollView {
|
||||
if hasSecurityRecommendations {
|
||||
securityRecommendationsSection
|
||||
}
|
||||
|
||||
currentSessionsSection
|
||||
|
||||
if !viewModel.viewState.otherSessionsViewData.isEmpty {
|
||||
otherSessionsSection
|
||||
}
|
||||
}
|
||||
.readableFrame()
|
||||
|
||||
if viewModel.viewState.linkDeviceButtonVisible {
|
||||
linkDeviceView
|
||||
.padding(.bottom, geometry.safeAreaInsets.bottom > 0 ? 20 : 36)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(theme.colors.system.ignoresSafeArea())
|
||||
@@ -158,6 +168,23 @@ struct UserSessionsOverview: View {
|
||||
}
|
||||
.accessibilityIdentifier("userSessionsOverviewOtherSection")
|
||||
}
|
||||
|
||||
/// The footer view containing link device button.
|
||||
var linkDeviceView: some View {
|
||||
VStack {
|
||||
Button {
|
||||
viewModel.send(viewAction: .linkDevice)
|
||||
} label: {
|
||||
Text(VectorL10n.userSessionsOverviewLinkDevice)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle(font: theme.fonts.bodySB))
|
||||
.padding(.top, 28)
|
||||
.padding(.bottom, 12)
|
||||
.padding(.horizontal, 16)
|
||||
.accessibilityIdentifier("linkDeviceButton")
|
||||
}
|
||||
.background(theme.colors.system.ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
Reference in New Issue
Block a user