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:
Aleksandrs Proskurins
2022-10-04 15:14:59 +03:00
88 changed files with 2246 additions and 415 deletions
@@ -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,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)
@@ -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)
}
}
@@ -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]?
}
@@ -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)
}
}
@@ -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)]
}()
}
}
@@ -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)
}
}
@@ -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))
}
}
@@ -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
@@ -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))
}
}
@@ -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?
}
@@ -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 {
@@ -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")
}
}