mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-23 10:02:46 +02:00
Merge branch 'develop' into aleksandrs/6786_inactive_sessions_screen
# Conflicts: # Riot/Assets/en.lproj/Vector.strings # Riot/Generated/Strings.swift # RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift # RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift # RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
This commit is contained in:
+14
-13
@@ -37,7 +37,8 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
init(parameters: UserSessionsOverviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
service = UserSessionsOverviewService(mxSession: parameters.session)
|
||||
let dataProvider = UserSessionsDataProvider(session: parameters.session)
|
||||
service = UserSessionsOverviewService(dataProvider: dataProvider)
|
||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||
hostingViewController = VectorHostingController(rootView: UserSessionsOverview(viewModel: viewModel.context))
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: hostingViewController)
|
||||
@@ -52,14 +53,14 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).")
|
||||
|
||||
switch result {
|
||||
case let .showOtherSessions(sessions: sessions, filter: filter):
|
||||
self.showOtherSessions(sessions: sessions, filterBy: filter)
|
||||
case let .showOtherSessions(sessionsInfo: sessionsInfo, filter: filter):
|
||||
self.showOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter)
|
||||
case .verifyCurrentSession:
|
||||
self.startVerifyCurrentSession()
|
||||
case let .showCurrentSessionOverview(session):
|
||||
self.showCurrentSessionOverview(session: session)
|
||||
case let .showUserSessionOverview(session):
|
||||
self.showUserSessionOverview(session: session)
|
||||
case let .showCurrentSessionOverview(sessionInfo):
|
||||
self.showCurrentSessionOverview(sessionInfo: sessionInfo)
|
||||
case let .showUserSessionOverview(sessionInfo):
|
||||
self.showUserSessionOverview(sessionInfo: sessionInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,20 +84,20 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
private func showOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||
completion?(.openOtherSessions(sessions: sessions, filter: filter))
|
||||
private func showOtherSessions(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||
completion?(.openOtherSessions(sessionsInfo: sessionsInfo, filter: filter))
|
||||
}
|
||||
|
||||
private func startVerifyCurrentSession() {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
private func showCurrentSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
private func showCurrentSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
private func showUserSessionOverview(session: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(session: session))
|
||||
private func showUserSessionOverview(sessionInfo: UserSessionInfo) {
|
||||
completion?(.openSessionOverview(sessionInfo: sessionInfo))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-12
@@ -20,31 +20,39 @@ import SwiftUI
|
||||
/// Using an enum for the screen allows you define the different state cases with
|
||||
/// the relevant associated data for each case.
|
||||
enum MockUserSessionsOverviewScreenState: MockScreenState, CaseIterable {
|
||||
case verifiedSession
|
||||
case currentSessionUnverified
|
||||
case currentSessionVerified
|
||||
case onlyUnverifiedSessions
|
||||
case onlyInactiveSessions
|
||||
case noOtherSessions
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
UserSessionsOverview.self
|
||||
}
|
||||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockUserSessionsOverviewScreenState] {
|
||||
// Each of the presence statuses
|
||||
[.verifiedSession]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let service = MockUserSessionsOverviewService()
|
||||
var service: UserSessionsOverviewServiceProtocol?
|
||||
switch self {
|
||||
case .verifiedSession:
|
||||
break
|
||||
case .currentSessionUnverified:
|
||||
service = MockUserSessionsOverviewService(mode: .currentSessionUnverified)
|
||||
case .currentSessionVerified:
|
||||
service = MockUserSessionsOverviewService(mode: .currentSessionVerified)
|
||||
case .onlyUnverifiedSessions:
|
||||
service = MockUserSessionsOverviewService(mode: .onlyUnverifiedSessions)
|
||||
case .onlyInactiveSessions:
|
||||
service = MockUserSessionsOverviewService(mode: .onlyInactiveSessions)
|
||||
case .noOtherSessions:
|
||||
service = MockUserSessionsOverviewService(mode: .noOtherSessions)
|
||||
}
|
||||
|
||||
guard let service = service else {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[service, viewModel],
|
||||
AnyView(UserSessionsOverview(viewModel: viewModel.context)
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixSDK
|
||||
|
||||
class UserSessionsDataProvider: UserSessionsDataProviderProtocol {
|
||||
private let session: MXSession
|
||||
|
||||
init(session: MXSession) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
var myDeviceId: String {
|
||||
session.myDeviceId
|
||||
}
|
||||
|
||||
var myUserId: String? {
|
||||
session.myUserId
|
||||
}
|
||||
|
||||
var activeAccounts: [MXKAccount] {
|
||||
MXKAccountManager.shared().activeAccounts
|
||||
}
|
||||
|
||||
func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void) {
|
||||
session.matrixRestClient.devices(completion: completion)
|
||||
}
|
||||
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? {
|
||||
session.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||
}
|
||||
|
||||
func accountData(for eventType: String) -> [AnyHashable : Any]? {
|
||||
session.accountData.accountData(forEventType: eventType)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2022 New Vector Ltd
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MatrixSDK
|
||||
|
||||
protocol UserSessionsDataProviderProtocol {
|
||||
var myDeviceId: String { get }
|
||||
|
||||
var myUserId: String? { get }
|
||||
|
||||
var activeAccounts: [MXKAccount] { get }
|
||||
|
||||
func devices(completion: @escaping (MXResponse<[MXDevice]>) -> Void)
|
||||
|
||||
func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo?
|
||||
|
||||
func accountData(for eventType: String) -> [AnyHashable: Any]?
|
||||
}
|
||||
+56
-28
@@ -21,12 +21,12 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
/// Delay after which session is considered inactive, 90 days
|
||||
private static let inactiveSessionDurationTreshold: TimeInterval = 90 * 86400
|
||||
|
||||
private let mxSession: MXSession
|
||||
private let dataProvider: UserSessionsDataProviderProtocol
|
||||
|
||||
private(set) var overviewData: UserSessionsOverviewData
|
||||
|
||||
init(mxSession: MXSession) {
|
||||
self.mxSession = mxSession
|
||||
init(dataProvider: UserSessionsDataProviderProtocol) {
|
||||
self.dataProvider = dataProvider
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
@@ -39,7 +39,7 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
// MARK: - Public
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
mxSession.matrixRestClient.devices { response in
|
||||
dataProvider.devices { response in
|
||||
switch response {
|
||||
case .success(let devices):
|
||||
self.overviewData = self.sessionsOverviewData(from: devices)
|
||||
@@ -61,26 +61,28 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
// MARK: - Private
|
||||
|
||||
private func setupInitialOverviewData() {
|
||||
let currentSessionInfo = currentSessionInfo()
|
||||
guard let currentSessionInfo = getCurrentSessionInfo() else {
|
||||
return
|
||||
}
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSessionInfo,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
unverifiedSessions: currentSessionInfo.isVerified ? [] : [currentSessionInfo],
|
||||
inactiveSessions: currentSessionInfo.isActive ? [] : [currentSessionInfo],
|
||||
otherSessions: [])
|
||||
}
|
||||
|
||||
private func currentSessionInfo() -> UserSessionInfo? {
|
||||
guard let mainAccount = MXKAccountManager.shared().activeAccounts.first,
|
||||
private func getCurrentSessionInfo() -> UserSessionInfo? {
|
||||
guard let mainAccount = dataProvider.activeAccounts.first,
|
||||
let device = mainAccount.device else {
|
||||
return nil
|
||||
}
|
||||
return sessionInfo(from: device, isCurrentSession: true)
|
||||
}
|
||||
|
||||
|
||||
private func sessionsOverviewData(from devices: [MXDevice]) -> UserSessionsOverviewData {
|
||||
let allSessions = devices
|
||||
.sorted { $0.lastSeenTs > $1.lastSeenTs }
|
||||
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == mxSession.myDeviceId) }
|
||||
.map { sessionInfo(from: $0, isCurrentSession: $0.deviceId == dataProvider.myDeviceId) }
|
||||
|
||||
return UserSessionsOverviewData(currentSession: allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: allSessions.filter { !$0.isVerified },
|
||||
@@ -90,33 +92,59 @@ class UserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
|
||||
private func sessionInfo(from device: MXDevice, isCurrentSession: Bool) -> UserSessionInfo {
|
||||
let isSessionVerified = deviceInfo(for: device.deviceId)?.trustLevel.isVerified ?? false
|
||||
|
||||
var lastSeenTs: TimeInterval?
|
||||
if device.lastSeenTs > 0 {
|
||||
lastSeenTs = TimeInterval(device.lastSeenTs / 1000)
|
||||
}
|
||||
|
||||
|
||||
let eventType = kMXAccountDataTypeClientInformation + "." + device.deviceId
|
||||
let appData = dataProvider.accountData(for: eventType)
|
||||
var userAgent: UserAgent?
|
||||
var isSessionActive = true
|
||||
if let lastSeenTimestamp = lastSeenTs {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - lastSeenTimestamp
|
||||
|
||||
if let lastSeenUserAgent = device.lastSeenUserAgent {
|
||||
userAgent = UserAgentParser.parse(lastSeenUserAgent)
|
||||
}
|
||||
|
||||
if device.lastSeenTs > 0 {
|
||||
let elapsedTime = Date().timeIntervalSince1970 - TimeInterval(device.lastSeenTs / 1000)
|
||||
isSessionActive = elapsedTime < Self.inactiveSessionDurationTreshold
|
||||
}
|
||||
|
||||
return UserSessionInfo(id: device.deviceId,
|
||||
name: device.displayName,
|
||||
deviceType: .unknown,
|
||||
isVerified: isSessionVerified,
|
||||
lastSeenIP: device.lastSeenIp,
|
||||
lastSeenTimestamp: lastSeenTs,
|
||||
|
||||
return UserSessionInfo(withDevice: device,
|
||||
applicationData: appData as? [String: String],
|
||||
userAgent: userAgent,
|
||||
isSessionVerified: isSessionVerified,
|
||||
isActive: isSessionActive,
|
||||
isCurrent: isCurrentSession)
|
||||
}
|
||||
|
||||
private func deviceInfo(for deviceId: String) -> MXDeviceInfo? {
|
||||
guard let userId = mxSession.myUserId else {
|
||||
guard let userId = dataProvider.myUserId else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return mxSession.crypto.device(withDeviceId: deviceId, ofUser: userId)
|
||||
return dataProvider.device(withDeviceId: deviceId, ofUser: userId)
|
||||
}
|
||||
}
|
||||
|
||||
extension UserSessionInfo {
|
||||
init(withDevice device: MXDevice,
|
||||
applicationData: [String: String]?,
|
||||
userAgent: UserAgent?,
|
||||
isSessionVerified: Bool,
|
||||
isActive: Bool,
|
||||
isCurrent: Bool) {
|
||||
self.init(id: device.deviceId,
|
||||
name: device.displayName,
|
||||
deviceType: userAgent?.deviceType ?? .unknown,
|
||||
isVerified: isSessionVerified,
|
||||
lastSeenIP: device.lastSeenIp,
|
||||
lastSeenTimestamp: device.lastSeenTs > 0 ? TimeInterval(device.lastSeenTs / 1000) : nil,
|
||||
applicationName: applicationData?["name"],
|
||||
applicationVersion: applicationData?["version"],
|
||||
applicationURL: applicationData?["url"],
|
||||
deviceModel: userAgent?.deviceModel,
|
||||
deviceOS: userAgent?.deviceOS,
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: userAgent?.clientName,
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
+103
-30
@@ -17,55 +17,128 @@
|
||||
import Foundation
|
||||
|
||||
class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol {
|
||||
enum Mode {
|
||||
case currentSessionUnverified
|
||||
case currentSessionVerified
|
||||
case onlyUnverifiedSessions
|
||||
case onlyInactiveSessions
|
||||
case noOtherSessions
|
||||
}
|
||||
|
||||
private let mode: Mode
|
||||
|
||||
var overviewData: UserSessionsOverviewData
|
||||
|
||||
init(mode: Mode = .currentSessionUnverified) {
|
||||
self.mode = mode
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: nil,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
}
|
||||
|
||||
func updateOverviewData(completion: @escaping (Result<UserSessionsOverviewData, Error>) -> Void) {
|
||||
let unverifiedSessions = buildSessions(verified: false, active: true)
|
||||
let inactiveSessions = buildSessions(verified: true, active: false)
|
||||
|
||||
switch mode {
|
||||
case .noOtherSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: [],
|
||||
otherSessions: [])
|
||||
case .onlyUnverifiedSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: unverifiedSessions + [currentSession],
|
||||
inactiveSessions: [],
|
||||
otherSessions: unverifiedSessions)
|
||||
case .onlyInactiveSessions:
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: [],
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: inactiveSessions)
|
||||
default:
|
||||
let otherSessions = unverifiedSessions + inactiveSessions + buildSessions(verified: true, active: true)
|
||||
|
||||
overviewData = UserSessionsOverviewData(currentSession: currentSession,
|
||||
unverifiedSessions: unverifiedSessions,
|
||||
inactiveSessions: inactiveSessions,
|
||||
otherSessions: otherSessions)
|
||||
}
|
||||
|
||||
completion(.success(overviewData))
|
||||
}
|
||||
|
||||
func sessionForIdentifier(_ sessionId: String) -> UserSessionInfo? {
|
||||
nil
|
||||
overviewData.otherSessions.first { $0.id == sessionId }
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var currentSession: UserSessionInfo {
|
||||
UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: mode == .currentSessionVerified,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: "Element iOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "iOS 15.5",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My iPhone",
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
}
|
||||
|
||||
init() {
|
||||
overviewData = UserSessionsOverviewData(currentSession: Self.allSessions.filter(\.isCurrent).first,
|
||||
unverifiedSessions: Self.allSessions.filter { !$0.isVerified },
|
||||
inactiveSessions: Self.allSessions.filter { !$0.isActive },
|
||||
otherSessions: Self.allSessions.filter { !$0.isCurrent })
|
||||
}
|
||||
|
||||
static var allSessions: [UserSessionInfo] = {
|
||||
[UserSessionInfo(id: "alice",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
isActive: true,
|
||||
isCurrent: true),
|
||||
UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
private func buildSessions(verified: Bool, active: Bool) -> [UserSessionInfo] {
|
||||
[UserSessionInfo(id: "1 verified: \(verified) active: \(active)",
|
||||
name: "macOS verified: \(verified) active: \(active)",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
|
||||
isActive: false,
|
||||
applicationName: "Element MacOS",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "macOS 12.5.1",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Mac",
|
||||
isActive: active,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "2",
|
||||
name: "Firefox on Windows",
|
||||
UserSessionInfo(id: "2 verified: \(verified) active: \(active)",
|
||||
name: "Firefox on Windows verified: \(verified) active: \(active)",
|
||||
deviceType: .web,
|
||||
isVerified: true,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "2.0.0.2",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
isActive: true,
|
||||
applicationName: "Element Web",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Windows 10",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Windows",
|
||||
isActive: active,
|
||||
isCurrent: false),
|
||||
UserSessionInfo(id: "3",
|
||||
name: "Android",
|
||||
UserSessionInfo(id: "3 verified: \(verified) active: \(active)",
|
||||
name: "Android verified: \(verified) active: \(active)",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
isVerified: verified,
|
||||
lastSeenIP: "3.0.0.3",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 10,
|
||||
isActive: true,
|
||||
applicationName: "Element Android",
|
||||
applicationVersion: "1.0.0",
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: "Android 4.0",
|
||||
lastSeenIPLocation: nil,
|
||||
deviceName: "My Phone",
|
||||
isActive: active,
|
||||
isCurrent: false)]
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
+34
-1
@@ -18,5 +18,38 @@ import RiotSwiftUI
|
||||
import XCTest
|
||||
|
||||
class UserSessionsOverviewUITests: MockScreenTestCase {
|
||||
// TODO:
|
||||
func testCurrentSessionUnverified() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionUnverified.title)
|
||||
|
||||
XCTAssertTrue(app.buttons["userSessionCardVerifyButton"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
|
||||
}
|
||||
|
||||
func testCurrentSessionVerified() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionVerified.title)
|
||||
|
||||
XCTAssertFalse(app.buttons["userSessionCardVerifyButton"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionCardViewDetails"].exists)
|
||||
}
|
||||
|
||||
func testOnlyUnverifiedSessions() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyUnverifiedSessions.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||
}
|
||||
|
||||
func testOnlyInactiveSessions() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyInactiveSessions.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||
XCTAssertTrue(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||
}
|
||||
|
||||
func testNoOtherSessions() {
|
||||
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.noOtherSessions.title)
|
||||
|
||||
XCTAssertFalse(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
|
||||
XCTAssertFalse(app.staticTexts["userSessionsOverviewOtherSection"].exists)
|
||||
}
|
||||
}
|
||||
|
||||
+59
-7
@@ -20,13 +20,65 @@ import XCTest
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserSessionsOverviewViewModelTests: XCTestCase {
|
||||
var service: MockUserSessionsOverviewService!
|
||||
var viewModel: UserSessionsOverviewViewModelProtocol!
|
||||
var context: UserSessionsOverviewViewModelType.Context!
|
||||
func testInitialStateEmpty() {
|
||||
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||
|
||||
XCTAssertNil(viewModel.state.currentSessionViewData)
|
||||
XCTAssertTrue(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||
XCTAssertTrue(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||
XCTAssertTrue(viewModel.state.otherSessionsViewData.isEmpty)
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
service = MockUserSessionsOverviewService()
|
||||
viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||
context = viewModel.context
|
||||
func testLoadOnDidAppear() {
|
||||
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||
viewModel.process(viewAction: .viewAppeared)
|
||||
|
||||
XCTAssertNotNil(viewModel.state.currentSessionViewData)
|
||||
XCTAssertFalse(viewModel.state.unverifiedSessionsViewData.isEmpty)
|
||||
XCTAssertFalse(viewModel.state.inactiveSessionsViewData.isEmpty)
|
||||
XCTAssertFalse(viewModel.state.otherSessionsViewData.isEmpty)
|
||||
}
|
||||
|
||||
func testSimpleActionProcessing() {
|
||||
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: MockUserSessionsOverviewService())
|
||||
|
||||
var result: UserSessionsOverviewViewModelResult?
|
||||
viewModel.completion = { action in
|
||||
result = action
|
||||
}
|
||||
|
||||
viewModel.process(viewAction: .verifyCurrentSession)
|
||||
XCTAssertEqual(result, .verifyCurrentSession)
|
||||
|
||||
viewModel.process(viewAction: .viewAllInactiveSessions)
|
||||
XCTAssertEqual(result, .showOtherSessions(sessionsInfo: [], filter: .inactive))
|
||||
}
|
||||
|
||||
func testShowSessionDetails() {
|
||||
let service = MockUserSessionsOverviewService()
|
||||
service.updateOverviewData { _ in }
|
||||
|
||||
let viewModel = UserSessionsOverviewViewModel(userSessionsOverviewService: service)
|
||||
|
||||
var result: UserSessionsOverviewViewModelResult?
|
||||
viewModel.completion = { action in
|
||||
result = action
|
||||
}
|
||||
|
||||
guard let currentSession = service.overviewData.currentSession else {
|
||||
XCTFail("The current session should be valid at this point")
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.process(viewAction: .viewCurrentSessionDetails)
|
||||
XCTAssertEqual(result, .showCurrentSessionOverview(sessionInfo: currentSession))
|
||||
|
||||
guard let randomSession = service.overviewData.otherSessions.randomElement() else {
|
||||
XCTFail("There should be other sessions")
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.process(viewAction: .tapUserSession(randomSession.id))
|
||||
XCTAssertEqual(result, .showUserSessionOverview(sessionInfo: randomSession))
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -19,17 +19,17 @@ import Foundation
|
||||
// MARK: - Coordinator
|
||||
|
||||
enum UserSessionsOverviewCoordinatorResult {
|
||||
case openSessionOverview(session: UserSessionInfo)
|
||||
case openOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case openSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case openOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserSessionsOverviewViewModelResult {
|
||||
case showOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
enum UserSessionsOverviewViewModelResult: Equatable {
|
||||
case showOtherSessions(sessionsInfo: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case verifyCurrentSession
|
||||
case showCurrentSessionOverview(session: UserSessionInfo)
|
||||
case showUserSessionOverview(session: UserSessionInfo)
|
||||
case showCurrentSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case showUserSessionOverview(sessionInfo: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
+8
-6
@@ -44,19 +44,21 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
assertionFailure("Missing current session")
|
||||
return
|
||||
}
|
||||
completion?(.showCurrentSessionOverview(session: currentSessionInfo))
|
||||
completion?(.showCurrentSessionOverview(sessionInfo: currentSessionInfo))
|
||||
case .viewAllUnverifiedSessions:
|
||||
showSessions(filteredBy: .unverified)
|
||||
// TODO: showSessions(filteredBy: .unverified)
|
||||
break
|
||||
case .viewAllInactiveSessions:
|
||||
showSessions(filteredBy: .inactive)
|
||||
case .viewAllOtherSessions:
|
||||
showSessions(filteredBy: .all)
|
||||
// TODO: showSessions(filteredBy: .all)
|
||||
break
|
||||
case .tapUserSession(let sessionId):
|
||||
guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else {
|
||||
assertionFailure("Missing session info")
|
||||
return
|
||||
}
|
||||
completion?(.showUserSessionOverview(session: session))
|
||||
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +70,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
state.otherSessionsViewData = userSessionsViewData.otherSessions.asViewData()
|
||||
|
||||
if let currentSessionInfo = userSessionsViewData.currentSession {
|
||||
state.currentSessionViewData = UserSessionCardViewData(session: currentSessionInfo)
|
||||
state.currentSessionViewData = UserSessionCardViewData(sessionInfo: currentSessionInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +95,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
}
|
||||
|
||||
private func showSessions(filteredBy filter: OtherUserSessionsFilter) {
|
||||
completion?(.showOtherSessions(sessions: userSessionsOverviewService.overviewData.otherSessions,
|
||||
completion?(.showOtherSessions(sessionsInfo: userSessionsOverviewService.overviewData.otherSessions,
|
||||
filter: filter))
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,6 +18,7 @@ import Foundation
|
||||
|
||||
/// View data for UserSessionListItem
|
||||
struct UserSessionListItemViewData: Identifiable, Hashable {
|
||||
|
||||
var id: String {
|
||||
sessionId
|
||||
}
|
||||
@@ -29,6 +30,6 @@ struct UserSessionListItemViewData: Identifiable, Hashable {
|
||||
let sessionDetails: String
|
||||
|
||||
let deviceAvatarViewData: DeviceAvatarViewData
|
||||
|
||||
|
||||
let sessionDetailsIcon: String?
|
||||
}
|
||||
|
||||
+4
-8
@@ -18,13 +18,9 @@ import Foundation
|
||||
|
||||
struct UserSessionListItemViewDataFactory {
|
||||
|
||||
private static let userSessionNameFormatter = UserSessionNameFormatter()
|
||||
private static let lastActivityDateFormatter = UserSessionLastActivityFormatter()
|
||||
private static let inactiveSessionDateFormatter = InactiveUserSessionLastActivityFormatter()
|
||||
|
||||
func create(from session: UserSessionInfo) -> UserSessionListItemViewData {
|
||||
let sessionName = UserSessionListItemViewDataFactory.userSessionNameFormatter.sessionName(deviceType: session.deviceType,
|
||||
sessionDisplayName: session.name)
|
||||
let sessionName = UserSessionNameFormatter.sessionName(deviceType: session.deviceType,
|
||||
sessionDisplayName: session.name)
|
||||
let sessionDetails = buildSessionDetails(isVerified: session.isVerified,
|
||||
lastActivityDate: session.lastSeenTimestamp,
|
||||
isActive: session.isActive)
|
||||
@@ -47,7 +43,7 @@ struct UserSessionListItemViewDataFactory {
|
||||
|
||||
private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String {
|
||||
if let lastActivityDate = lastActivityDate {
|
||||
let lastActivityDateString = Self.inactiveSessionDateFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
let lastActivityDateString = InactiveUserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString)
|
||||
}
|
||||
return VectorL10n.userInactiveSessionItem
|
||||
@@ -61,7 +57,7 @@ struct UserSessionListItemViewDataFactory {
|
||||
var lastActivityDateString: String?
|
||||
|
||||
if let lastActivityDate = lastActivityDate {
|
||||
lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
lastActivityDateString = UserSessionLastActivityFormatter.lastActivityDateString(from: lastActivityDate)
|
||||
}
|
||||
|
||||
if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false {
|
||||
|
||||
+35
-35
@@ -23,11 +23,13 @@ struct UserSessionsOverview: View {
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
securityRecommendationsSection
|
||||
if hasSecurityRecommendations {
|
||||
securityRecommendationsSection
|
||||
}
|
||||
|
||||
currentSessionsSection
|
||||
|
||||
if viewModel.viewState.otherSessionsViewData.isEmpty == false {
|
||||
if !viewModel.viewState.otherSessionsViewData.isEmpty {
|
||||
otherSessionsSection
|
||||
}
|
||||
}
|
||||
@@ -40,41 +42,39 @@ struct UserSessionsOverview: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var securityRecommendationsSection: some View {
|
||||
if hasSecurityRecommendations {
|
||||
SwiftUI.Section {
|
||||
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
||||
SecurityRecommendationCard(style: .unverified,
|
||||
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
||||
viewModel.send(viewAction: .viewAllUnverifiedSessions)
|
||||
}
|
||||
SwiftUI.Section {
|
||||
if !viewModel.viewState.unverifiedSessionsViewData.isEmpty {
|
||||
SecurityRecommendationCard(style: .unverified,
|
||||
sessionCount: viewModel.viewState.unverifiedSessionsViewData.count) {
|
||||
viewModel.send(viewAction: .viewAllUnverifiedSessions)
|
||||
}
|
||||
|
||||
if !viewModel.viewState.inactiveSessionsViewData.isEmpty {
|
||||
SecurityRecommendationCard(style: .inactive,
|
||||
sessionCount: viewModel.viewState.inactiveSessionsViewData.count) {
|
||||
viewModel.send(viewAction: .viewAllInactiveSessions)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
VStack(alignment: .leading) {
|
||||
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionTitle)
|
||||
.textCase(.uppercase)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.padding(.bottom, 8.0)
|
||||
|
||||
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionInfo)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.padding(.bottom, 12.0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 24)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
|
||||
if !viewModel.viewState.inactiveSessionsViewData.isEmpty {
|
||||
SecurityRecommendationCard(style: .inactive,
|
||||
sessionCount: viewModel.viewState.inactiveSessionsViewData.count) {
|
||||
viewModel.send(viewAction: .viewAllInactiveSessions)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
VStack(alignment: .leading) {
|
||||
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionTitle)
|
||||
.textCase(.uppercase)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.padding(.bottom, 8.0)
|
||||
|
||||
Text(VectorL10n.userSessionsOverviewSecurityRecommendationsSectionInfo)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.secondaryContent)
|
||||
.padding(.bottom, 12.0)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 24)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.accessibilityIdentifier("userSessionsOverviewSecurityRecommendationsSection")
|
||||
}
|
||||
|
||||
var hasSecurityRecommendations: Bool {
|
||||
@@ -102,10 +102,9 @@ struct UserSessionsOverview: View {
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var otherSessionsSection: some View {
|
||||
SwiftUI.Section {
|
||||
// Device list
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(viewModel.viewState.otherSessionsViewData) { viewData in
|
||||
UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
|
||||
@@ -131,6 +130,7 @@ struct UserSessionsOverview: View {
|
||||
.padding(.horizontal, 16.0)
|
||||
.padding(.top, 24.0)
|
||||
}
|
||||
.accessibilityIdentifier("userSessionsOverviewOtherSection")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user