mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-09 09:27:42 +02:00
Merge pull request #6860 from vector-im/aleksandrs/6838_filter_sessions
Device Manager: Filter sessions
This commit is contained in:
@@ -141,7 +141,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
||||
return UserSessionOverviewCoordinator(parameters: parameters)
|
||||
}
|
||||
|
||||
private func openOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||
private func openOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: UserOtherSessionsFilter) {
|
||||
let title = filter == .all ? VectorL10n.userSessionsOverviewOtherSessionsSectionTitle : VectorL10n.userOtherSessionSecurityRecommendationTitle
|
||||
let coordinator = createOtherSessionsCoordinator(sessionInfos: sessionInfos,
|
||||
filterBy: filter,
|
||||
@@ -157,7 +157,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
|
||||
}
|
||||
|
||||
private func createOtherSessionsCoordinator(sessionInfos: [UserSessionInfo],
|
||||
filterBy filter: OtherUserSessionsFilter,
|
||||
filterBy filter: UserOtherSessionsFilter,
|
||||
title: String) -> UserOtherSessionsCoordinator {
|
||||
let parameters = UserOtherSessionsCoordinatorParameters(sessionInfos: sessionInfos,
|
||||
filter: filter,
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import SwiftUI
|
||||
|
||||
struct UserOtherSessionsCoordinatorParameters {
|
||||
let sessionInfos: [UserSessionInfo]
|
||||
let filter: OtherUserSessionsFilter
|
||||
let filter: UserOtherSessionsFilter
|
||||
let title: String
|
||||
}
|
||||
|
||||
|
||||
+41
-1
@@ -27,6 +27,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
case all
|
||||
case inactiveSessions
|
||||
case unverifiedSessions
|
||||
case verifiedSessions
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
@@ -36,7 +37,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockUserOtherSessionsScreenState] {
|
||||
// Each of the presence statuses
|
||||
[.all, .inactiveSessions, .unverifiedSessions]
|
||||
[.all, .inactiveSessions, .unverifiedSessions, .verifiedSessions]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
@@ -55,6 +56,10 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
viewModel = UserOtherSessionsViewModel(sessionInfos: unverifiedSessions(),
|
||||
filter: .unverified,
|
||||
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||
case .verifiedSessions:
|
||||
viewModel = UserOtherSessionsViewModel(sessionInfos: verifiedSessions(),
|
||||
filter: .verified,
|
||||
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
@@ -167,6 +172,41 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
|
||||
isCurrent: false)]
|
||||
}
|
||||
|
||||
private func verifiedSessions() -> [UserSessionInfo] {
|
||||
[UserSessionInfo(id: "0",
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
verificationState: .verified,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: nil,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: nil,
|
||||
clientVersion: nil,
|
||||
isActive: true,
|
||||
isCurrent: true),
|
||||
UserSessionInfo(id: "1",
|
||||
name: "macOS",
|
||||
deviceType: .desktop,
|
||||
verificationState: .verified,
|
||||
lastSeenIP: "1.0.0.1",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 8_000_000,
|
||||
applicationName: nil,
|
||||
applicationVersion: nil,
|
||||
applicationURL: nil,
|
||||
deviceModel: nil,
|
||||
deviceOS: nil,
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: nil,
|
||||
clientVersion: nil,
|
||||
isActive: true,
|
||||
isCurrent: false)]
|
||||
}
|
||||
|
||||
private func allSessions() -> [UserSessionInfo] {
|
||||
[UserSessionInfo(id: "0",
|
||||
name: "iOS",
|
||||
|
||||
+9
-2
@@ -21,7 +21,7 @@ class UserOtherSessionsUITests: MockScreenTestCase {
|
||||
func test_whenOtherSessionsWithInactiveSessionFilterPresented_correctHeaderDisplayed() {
|
||||
app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.inactiveSessions.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userOtherSessionFilterMenuInactive].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo].exists)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ class UserOtherSessionsUITests: MockScreenTestCase {
|
||||
func test_whenOtherSessionsWithUnverifiedSessionFilterPresented_correctHeaderDisplayed() {
|
||||
app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.unverifiedSessions.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsUnverifiedTitle].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionUnverifiedShort].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userOtherSessionUnverifiedSessionsHeaderSubtitle].exists)
|
||||
}
|
||||
|
||||
@@ -49,4 +49,11 @@ class UserOtherSessionsUITests: MockScreenTestCase {
|
||||
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewOtherSessionsSectionInfo].exists)
|
||||
}
|
||||
|
||||
func test_whenOtherSessionsWithVerifiedSessionFilterPresented_correctHeaderDisplayed() {
|
||||
app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.verifiedSessions.title)
|
||||
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionVerifiedShort].exists)
|
||||
XCTAssertTrue(app.staticTexts[VectorL10n.userOtherSessionVerifiedSessionsHeaderSubtitle].exists)
|
||||
}
|
||||
}
|
||||
|
||||
+103
-26
@@ -19,12 +19,27 @@ import XCTest
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserOtherSessionsViewModelTests: XCTestCase {
|
||||
private let unverifiedSectionHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionUnverifiedShort,
|
||||
subtitle: VectorL10n.userOtherSessionUnverifiedSessionsHeaderSubtitle,
|
||||
iconName: Asset.Images.userOtherSessionsUnverified.name)
|
||||
|
||||
private let inactiveSectionHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userOtherSessionFilterMenuInactive,
|
||||
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
|
||||
iconName: Asset.Images.userOtherSessionsInactive.name)
|
||||
|
||||
private let allSectionHeader = UserOtherSessionsHeaderViewData(title: nil,
|
||||
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
|
||||
iconName: nil)
|
||||
|
||||
private let verifiedSectionHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userOtherSessionFilterMenuVerified,
|
||||
subtitle: VectorL10n.userOtherSessionVerifiedSessionsHeaderSubtitle,
|
||||
iconName: Asset.Images.userOtherSessionsVerified.name)
|
||||
|
||||
func test_whenUserOtherSessionSelectedProcessed_completionWithShowUserSessionOverviewCalled() {
|
||||
let expectedUserSessionInfo = createUserSessionInfo(sessionId: "session 2")
|
||||
let sut = UserOtherSessionsViewModel(sessionInfos: [createUserSessionInfo(sessionId: "session 1"),
|
||||
expectedUserSessionInfo],
|
||||
filter: .inactive,
|
||||
title: "Title")
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
|
||||
expectedUserSessionInfo]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .inactive)
|
||||
|
||||
var modelResult: UserOtherSessionsViewModelResult?
|
||||
sut.completion = { result in
|
||||
@@ -35,40 +50,102 @@ 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")
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isActive: false),
|
||||
createUserSessionInfo(sessionId: "session 2", isActive: false)]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .inactive)
|
||||
|
||||
let expectedHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
|
||||
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
|
||||
iconName: Asset.Images.userOtherSessionsInactive.name)
|
||||
let expectedItems = sessionInfos.filter { !$0.isActive }.asViewData()
|
||||
let expectedState = UserOtherSessionsViewState(title: "Title",
|
||||
sections: [.sessionItems(header: expectedHeader, items: expectedItems)])
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .inactive),
|
||||
title: "Title",
|
||||
sections: [.sessionItems(header: inactiveSectionHeader, items: expectedItems)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
func test_whenModelCreated_withAllFilter_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"), createUserSessionInfo(sessionId: "session 2")]
|
||||
let sut = UserOtherSessionsViewModel(sessionInfos: sessionInfos,
|
||||
filter: .all,
|
||||
title: "Title")
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
|
||||
createUserSessionInfo(sessionId: "session 2")]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .all)
|
||||
|
||||
let expectedHeader = UserOtherSessionsHeaderViewData(title: nil,
|
||||
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
|
||||
iconName: nil)
|
||||
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
|
||||
let expectedState = UserOtherSessionsViewState(title: "Title",
|
||||
sections: [.sessionItems(header: expectedHeader, items: expectedItems)])
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .all),
|
||||
title: "Title",
|
||||
sections: [.sessionItems(header: allSectionHeader, items: expectedItems)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
private func createUserSessionInfo(sessionId: String) -> UserSessionInfo {
|
||||
func test_whenModelCreated_withUnverifiedFilter_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"),
|
||||
createUserSessionInfo(sessionId: "session 2")]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .unverified)
|
||||
|
||||
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .unverified),
|
||||
title: "Title",
|
||||
sections: [.sessionItems(header: unverifiedSectionHeader, items: expectedItems)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
func test_whenModelCreated_withVerifiedFilter_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: true),
|
||||
createUserSessionInfo(sessionId: "session 2", isVerified: true)]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .verified)
|
||||
|
||||
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .verified),
|
||||
title: "Title",
|
||||
sections: [.sessionItems(header: verifiedSectionHeader, items: expectedItems)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
func test_whenModelCreated_withVerifiedFilterWithNoVerifiedSessions_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: false),
|
||||
createUserSessionInfo(sessionId: "session 2", isVerified: false)]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .verified)
|
||||
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .verified),
|
||||
title: "Title",
|
||||
sections: [.emptySessionItems(header: verifiedSectionHeader, title: VectorL10n.userOtherSessionNoVerifiedSessions)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
func test_whenModelCreated_withUnverifiedFilterWithNoUnverifiedSessions_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isVerified: true),
|
||||
createUserSessionInfo(sessionId: "session 2", isVerified: true)]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .unverified)
|
||||
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .unverified),
|
||||
title: "Title",
|
||||
sections: [.emptySessionItems(header: unverifiedSectionHeader, title: VectorL10n.userOtherSessionNoUnverifiedSessions)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
func test_whenModelCreated_withInactiveFilterWithNoInactiveSessions_viewStateIsCorrect() {
|
||||
let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isActive: true),
|
||||
createUserSessionInfo(sessionId: "session 2", isActive: true)]
|
||||
let sut = createSUT(sessionInfos: sessionInfos, filter: .inactive)
|
||||
|
||||
let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .inactive),
|
||||
title: "Title",
|
||||
sections: [.emptySessionItems(header: inactiveSectionHeader, title: VectorL10n.userOtherSessionNoInactiveSessions)])
|
||||
XCTAssertEqual(sut.state, expectedState)
|
||||
}
|
||||
|
||||
private func createSUT(sessionInfos: [UserSessionInfo],
|
||||
filter: UserOtherSessionsFilter,
|
||||
title: String = "Title") -> UserOtherSessionsViewModel {
|
||||
UserOtherSessionsViewModel(sessionInfos: sessionInfos,
|
||||
filter: filter,
|
||||
title: title)
|
||||
}
|
||||
|
||||
private func createUserSessionInfo(sessionId: String,
|
||||
isVerified: Bool = false,
|
||||
isActive: Bool = true,
|
||||
isCurrent: Bool = false) -> UserSessionInfo {
|
||||
UserSessionInfo(id: sessionId,
|
||||
name: "iOS",
|
||||
deviceType: .mobile,
|
||||
verificationState: .unverified,
|
||||
verificationState: isVerified ? .verified : .unverified,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
|
||||
applicationName: nil,
|
||||
@@ -79,7 +156,7 @@ class UserOtherSessionsViewModelTests: XCTestCase {
|
||||
lastSeenIPLocation: nil,
|
||||
clientName: nil,
|
||||
clientVersion: nil,
|
||||
isActive: true,
|
||||
isCurrent: true)
|
||||
isActive: isActive,
|
||||
isCurrent: isCurrent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum UserOtherSessionsFilter: Identifiable, Equatable, CaseIterable {
|
||||
var id: Self { self }
|
||||
case all
|
||||
case verified
|
||||
case unverified
|
||||
case inactive
|
||||
}
|
||||
|
||||
extension UserOtherSessionsFilter {
|
||||
var menuLocalizedName: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return VectorL10n.userOtherSessionFilterMenuAll
|
||||
case .verified:
|
||||
return VectorL10n.userOtherSessionFilterMenuVerified
|
||||
case .unverified:
|
||||
return VectorL10n.userOtherSessionFilterMenuUnverified
|
||||
case .inactive:
|
||||
return VectorL10n.userOtherSessionFilterMenuInactive
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,18 +31,26 @@ enum UserOtherSessionsViewModelResult: Equatable {
|
||||
// MARK: View
|
||||
|
||||
struct UserOtherSessionsViewState: BindableState, Equatable {
|
||||
var bindings: UserOtherSessionsBindings
|
||||
let title: String
|
||||
var sections: [UserOtherSessionsSection]
|
||||
}
|
||||
|
||||
struct UserOtherSessionsBindings: Equatable {
|
||||
var filter: UserOtherSessionsFilter
|
||||
}
|
||||
|
||||
enum UserOtherSessionsSection: Hashable, Identifiable {
|
||||
var id: Self {
|
||||
self
|
||||
}
|
||||
|
||||
case sessionItems(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData])
|
||||
case emptySessionItems(header: UserOtherSessionsHeaderViewData, title: String)
|
||||
}
|
||||
|
||||
enum UserOtherSessionsViewAction {
|
||||
case userOtherSessionSelected(sessionId: String)
|
||||
case filterWasChanged
|
||||
case clearFilter
|
||||
}
|
||||
|
||||
+45
-18
@@ -18,22 +18,18 @@ import SwiftUI
|
||||
|
||||
typealias UserOtherSessionsViewModelType = StateStoreViewModel<UserOtherSessionsViewState, UserOtherSessionsViewAction>
|
||||
|
||||
enum OtherUserSessionsFilter {
|
||||
case all
|
||||
case inactive
|
||||
case unverified
|
||||
}
|
||||
|
||||
class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol {
|
||||
var completion: ((UserOtherSessionsViewModelResult) -> Void)?
|
||||
private let sessionInfos: [UserSessionInfo]
|
||||
|
||||
init(sessionInfos: [UserSessionInfo],
|
||||
filter: OtherUserSessionsFilter,
|
||||
filter: UserOtherSessionsFilter,
|
||||
title: String) {
|
||||
self.sessionInfos = sessionInfos
|
||||
super.init(initialViewState: UserOtherSessionsViewState(title: title, sections: []))
|
||||
updateViewState(sessionInfos: sessionInfos, filter: filter)
|
||||
super.init(initialViewState: UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: filter),
|
||||
title: title,
|
||||
sections: []))
|
||||
updateViewState()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
@@ -46,18 +42,29 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
|
||||
return
|
||||
}
|
||||
completion?(.showUserSessionOverview(sessionInfo: session))
|
||||
case .filterWasChanged:
|
||||
updateViewState()
|
||||
case .clearFilter:
|
||||
state.bindings.filter = .all
|
||||
updateViewState()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func updateViewState(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter) {
|
||||
let sectionItems = createSectionItems(sessionInfos: sessionInfos, filter: filter)
|
||||
let sectionHeader = createHeaderData(filter: filter)
|
||||
state.sections = [.sessionItems(header: sectionHeader, items: sectionItems)]
|
||||
private func updateViewState() {
|
||||
let sectionItems = createSectionItems(sessionInfos: sessionInfos, filter: state.bindings.filter)
|
||||
let sectionHeader = createHeaderData(filter: state.bindings.filter)
|
||||
if sectionItems.isEmpty {
|
||||
state.sections = [.emptySessionItems(header: sectionHeader,
|
||||
title: noSessionsTitle(filter: state.bindings.filter))]
|
||||
} else {
|
||||
state.sections = [.sessionItems(header: sectionHeader,
|
||||
items: sectionItems)]
|
||||
}
|
||||
}
|
||||
|
||||
private func createSectionItems(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter) -> [UserSessionListItemViewData] {
|
||||
private func createSectionItems(sessionInfos: [UserSessionInfo], filter: UserOtherSessionsFilter) -> [UserSessionListItemViewData] {
|
||||
filterSessions(sessionInfos: sessionInfos, by: filter)
|
||||
.map {
|
||||
UserSessionListItemViewDataFactory().create(from: $0,
|
||||
@@ -65,7 +72,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
|
||||
}
|
||||
}
|
||||
|
||||
private func filterSessions(sessionInfos: [UserSessionInfo], by filter: OtherUserSessionsFilter) -> [UserSessionInfo] {
|
||||
private func filterSessions(sessionInfos: [UserSessionInfo], by filter: UserOtherSessionsFilter) -> [UserSessionInfo] {
|
||||
switch filter {
|
||||
case .all:
|
||||
return sessionInfos.filter { !$0.isCurrent }
|
||||
@@ -73,23 +80,43 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
|
||||
return sessionInfos.filter { !$0.isActive }
|
||||
case .unverified:
|
||||
return sessionInfos.filter { $0.verificationState != .verified }
|
||||
case .verified:
|
||||
return sessionInfos.filter { $0.verificationState == .verified }
|
||||
}
|
||||
}
|
||||
|
||||
private func createHeaderData(filter: OtherUserSessionsFilter) -> UserOtherSessionsHeaderViewData {
|
||||
private func createHeaderData(filter: UserOtherSessionsFilter) -> UserOtherSessionsHeaderViewData {
|
||||
switch filter {
|
||||
case .all:
|
||||
return UserOtherSessionsHeaderViewData(title: nil,
|
||||
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
|
||||
iconName: nil)
|
||||
case .inactive:
|
||||
return UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
|
||||
return UserOtherSessionsHeaderViewData(title: VectorL10n.userOtherSessionFilterMenuInactive,
|
||||
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
|
||||
iconName: Asset.Images.userOtherSessionsInactive.name)
|
||||
case .unverified:
|
||||
return UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsUnverifiedTitle,
|
||||
return UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionUnverifiedShort,
|
||||
subtitle: VectorL10n.userOtherSessionUnverifiedSessionsHeaderSubtitle,
|
||||
iconName: Asset.Images.userOtherSessionsUnverified.name)
|
||||
case .verified:
|
||||
return UserOtherSessionsHeaderViewData(title: VectorL10n.userOtherSessionFilterMenuVerified,
|
||||
subtitle: VectorL10n.userOtherSessionVerifiedSessionsHeaderSubtitle,
|
||||
iconName: Asset.Images.userOtherSessionsVerified.name)
|
||||
}
|
||||
}
|
||||
|
||||
private func noSessionsTitle(filter: UserOtherSessionsFilter) -> String {
|
||||
switch filter {
|
||||
case .all:
|
||||
assertionFailure("The view is not intended to be displayed without any session")
|
||||
return ""
|
||||
case .verified:
|
||||
return VectorL10n.userOtherSessionNoVerifiedSessions
|
||||
case .unverified:
|
||||
return VectorL10n.userOtherSessionNoUnverifiedSessions
|
||||
case .inactive:
|
||||
return VectorL10n.userOtherSessionNoInactiveSessions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,12 +27,32 @@ struct UserOtherSessions: View {
|
||||
switch section {
|
||||
case let .sessionItems(header: header, items: items):
|
||||
createSessionItemsSection(header: header, items: items)
|
||||
case let .emptySessionItems(header: header, title: title):
|
||||
createEmptySessionsItemsSection(header: header, title: title)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(theme.colors.system.ignoresSafeArea())
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(viewModel.viewState.title)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
Picker("", selection: $viewModel.filter) {
|
||||
ForEach(UserOtherSessionsFilter.allCases) { filter in
|
||||
Text(filter.menuLocalizedName).tag(filter)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.onChange(of: viewModel.filter) { _ in
|
||||
viewModel.send(viewAction: .filterWasChanged)
|
||||
}
|
||||
} label: {
|
||||
Image(viewModel.filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name)
|
||||
}
|
||||
.accessibilityLabel(VectorL10n.userOtherSessionFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createSessionItemsSection(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) -> some View {
|
||||
@@ -46,11 +66,43 @@ struct UserOtherSessions: View {
|
||||
}
|
||||
.background(theme.colors.background)
|
||||
} header: {
|
||||
UserOtherSessionsHeaderView(viewData: header)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 24.0)
|
||||
headerView(header: header)
|
||||
}
|
||||
}
|
||||
|
||||
private func createEmptySessionsItemsSection(header: UserOtherSessionsHeaderViewData, title: String) -> some View {
|
||||
SwiftUI.Section {
|
||||
VStack {
|
||||
Text(title)
|
||||
.font(theme.fonts.footnote)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.padding(.bottom, 20)
|
||||
Button {
|
||||
viewModel.send(viewAction: .clearFilter)
|
||||
} label: {
|
||||
VStack(spacing: 0) {
|
||||
SeparatorLine()
|
||||
Text(VectorL10n.userOtherSessionClearFilter)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.accent)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(.vertical, 11)
|
||||
SeparatorLine()
|
||||
}
|
||||
.background(theme.colors.background)
|
||||
}
|
||||
}
|
||||
|
||||
} header: {
|
||||
headerView(header: header)
|
||||
}
|
||||
}
|
||||
|
||||
private func headerView(header: UserOtherSessionsHeaderViewData) -> some View {
|
||||
UserOtherSessionsHeaderView(viewData: header)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 24.0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable {
|
||||
loadingIndicator = nil
|
||||
}
|
||||
|
||||
private func showOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
|
||||
private func showOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: UserOtherSessionsFilter) {
|
||||
completion?(.openOtherSessions(sessionInfos: sessionInfos, filter: filter))
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -23,14 +23,14 @@ enum UserSessionsOverviewCoordinatorResult {
|
||||
case renameSession(UserSessionInfo)
|
||||
case logoutOfSession(UserSessionInfo)
|
||||
case openSessionOverview(sessionInfo: UserSessionInfo)
|
||||
case openOtherSessions(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case openOtherSessions(sessionInfos: [UserSessionInfo], filter: UserOtherSessionsFilter)
|
||||
case linkDevice
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserSessionsOverviewViewModelResult: Equatable {
|
||||
case showOtherSessions(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter)
|
||||
case showOtherSessions(sessionInfos: [UserSessionInfo], filter: UserOtherSessionsFilter)
|
||||
case verifyCurrentSession
|
||||
case renameSession(UserSessionInfo)
|
||||
case logoutOfSession(UserSessionInfo)
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
|
||||
}
|
||||
}
|
||||
|
||||
private func showSessions(filteredBy filter: OtherUserSessionsFilter) {
|
||||
private func showSessions(filteredBy filter: UserOtherSessionsFilter) {
|
||||
completion?(.showOtherSessions(sessionInfos: userSessionsOverviewService.sessionInfos,
|
||||
filter: filter))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user