From 76e2a829486c01ca8bf3db429c0048d096267154 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 11 Oct 2022 08:42:28 +0300 Subject: [PATCH 01/12] Filter button --- .../Contents.json | 12 ++++++ .../user_other_sessions_filter.svg | 3 ++ .../Contents.json | 21 +++++++++++ .../user_other_sessions_filter_selected.svg | 6 +++ Riot/Generated/Images.swift | 2 + .../OtherUserSessionsFilter.swift | 37 +++++++++++++++++++ .../UserOtherSessionsModels.swift | 6 +++ .../UserOtherSessionsViewModel.swift | 16 ++++---- .../View/UserOtherSessions.swift | 17 +++++++++ 9 files changed, 111 insertions(+), 9 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/user_other_sessions_filter.svg create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/user_other_sessions_filter_selected.svg create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/Contents.json new file mode 100644 index 000000000..81ee52eeb --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_other_sessions_filter.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/user_other_sessions_filter.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/user_other_sessions_filter.svg new file mode 100644 index 000000000..a2b8549a1 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter.imageset/user_other_sessions_filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json new file mode 100644 index 000000000..54a3ea24f --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "user_other_sessions_filter_selected.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/user_other_sessions_filter_selected.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/user_other_sessions_filter_selected.svg new file mode 100644 index 000000000..f964fdd1c --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/user_other_sessions_filter_selected.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 24d2164be..03fb73985 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -107,6 +107,8 @@ internal class Asset: NSObject { internal static let deviceTypeMobile = ImageAsset(name: "device_type_mobile") internal static let deviceTypeUnknown = ImageAsset(name: "device_type_unknown") internal static let deviceTypeWeb = ImageAsset(name: "device_type_web") + internal static let userOtherSessionsFilter = ImageAsset(name: "user_other_sessions_filter") + internal static let userOtherSessionsFilterSelected = ImageAsset(name: "user_other_sessions_filter_selected") internal static let userOtherSessionsInactive = ImageAsset(name: "user_other_sessions_inactive") internal static let userOtherSessionsUnverified = ImageAsset(name: "user_other_sessions_unverified") internal static let userSessionListItemInactiveSession = ImageAsset(name: "user_session_list_item_inactive_session") diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift new file mode 100644 index 000000000..09315e49f --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift @@ -0,0 +1,37 @@ +// +// 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 OtherUserSessionsFilter: Identifiable, Equatable, CaseIterable { + var id: Self { self } + case all + case inactive + case unverified +} + +extension OtherUserSessionsFilter { + var menuLocalizedName: String { + switch self { + case .all: + return "All sessions" + case .inactive: + return "Inactive" + case .unverified: + return "Unverified" + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index 53679d990..50b15b054 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -31,10 +31,15 @@ enum UserOtherSessionsViewModelResult: Equatable { // MARK: View struct UserOtherSessionsViewState: BindableState, Equatable { + var bindings: UserOtherSessionsBindings let title: String var sections: [UserOtherSessionsSection] } +struct UserOtherSessionsBindings: Equatable { + var filter: OtherUserSessionsFilter +} + enum UserOtherSessionsSection: Hashable, Identifiable { var id: Self { self @@ -45,4 +50,5 @@ enum UserOtherSessionsSection: Hashable, Identifiable { enum UserOtherSessionsViewAction { case userOtherSessionSelected(sessionId: String) + case filerWasChanged } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 040948415..d5411be5c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -18,12 +18,6 @@ import SwiftUI typealias UserOtherSessionsViewModelType = StateStoreViewModel -enum OtherUserSessionsFilter { - case all - case inactive - case unverified -} - class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol { var completion: ((UserOtherSessionsViewModelResult) -> Void)? private let sessionInfos: [UserSessionInfo] @@ -32,8 +26,10 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi filter: OtherUserSessionsFilter, 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(filter: filter) } // MARK: - Public @@ -46,12 +42,14 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi return } completion?(.showUserSessionOverview(sessionInfo: session)) + case .filerWasChanged: + updateViewState(filter: state.bindings.filter) } } // MARK: - Private - private func updateViewState(sessionInfos: [UserSessionInfo], filter: OtherUserSessionsFilter) { + private func updateViewState(filter: OtherUserSessionsFilter) { let sectionItems = createSectionItems(sessionInfos: sessionInfos, filter: filter) let sectionHeader = createHeaderData(filter: filter) state.sections = [.sessionItems(header: sectionHeader, items: sectionItems)] diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 6bcc7d034..6ed50f7e6 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -33,6 +33,23 @@ struct UserOtherSessions: View { .background(theme.colors.system.ignoresSafeArea()) .frame(maxHeight: .infinity) .navigationTitle(viewModel.viewState.title) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Menu { + Picker("Filter menu", selection: $viewModel.filter) { + ForEach(OtherUserSessionsFilter.allCases) { filter in + Text(filter.menuLocalizedName).tag(filter) + } + } + .labelsHidden() + .onChange(of: viewModel.filter) { _ in + viewModel.send(viewAction: .filerWasChanged) + } + } label: { + Image(viewModel.filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name) + } + } + } } private func createSessionItemsSection(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) -> some View { From 669a71c4d0d0bb3cfd0d665696a469b025e9dcbc Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 11 Oct 2022 09:42:07 +0300 Subject: [PATCH 02/12] Verified sessions --- .../Contents.json | 12 +++++++++++ .../user_other_sessions_verified.svg | 4 ++++ Riot/Assets/en.lproj/Vector.strings | 5 +++++ Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 20 +++++++++++++++++++ .../OtherUserSessionsFilter.swift | 13 +++++++----- .../Test/UI/UserOtherSessionsUITests.swift | 4 ++-- .../UserOtherSessionsViewModel.swift | 10 ++++++++-- 8 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/user_other_sessions_verified.svg diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/Contents.json new file mode 100644 index 000000000..fd25f3b8e --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_other_sessions_verified.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/user_other_sessions_verified.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/user_other_sessions_verified.svg new file mode 100644 index 000000000..793d65784 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_verified.imageset/user_other_sessions_verified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 3675d1152..c81878669 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2431,7 +2431,12 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_security_recommendation_title" = "Security recommendation"; "user_other_session_unverified_sessions_header_subtitle" = "Verify your sessions for enhanced secure messaging or sign out from those you don’t recognize or use anymore."; "user_other_session_unverified_current_session_details" = "%@ · Your current session"; +"user_other_session_verified_sessions_header_subtitle" = "For best security, sign out from any session that you don’t recognize or use anymore."; +"user_other_session_filter_menu_all" = "All sessions"; +"user_other_session_filter_menu_verified" = "Verified"; +"user_other_session_filter_menu_unverified" = "Unverified"; +"user_other_session_filter_menu_inactive" = "Inactive"; // First item is client name and second item is session display name "user_session_name" = "%@: %@"; diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 03fb73985..762248059 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -111,6 +111,7 @@ internal class Asset: NSObject { internal static let userOtherSessionsFilterSelected = ImageAsset(name: "user_other_sessions_filter_selected") internal static let userOtherSessionsInactive = ImageAsset(name: "user_other_sessions_inactive") internal static let userOtherSessionsUnverified = ImageAsset(name: "user_other_sessions_unverified") + internal static let userOtherSessionsVerified = ImageAsset(name: "user_other_sessions_verified") internal static let userSessionListItemInactiveSession = ImageAsset(name: "user_session_list_item_inactive_session") internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified") internal static let userSessionVerified = ImageAsset(name: "user_session_verified") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 584dfd509..16022e641 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8623,6 +8623,22 @@ public class VectorL10n: NSObject { public static func userInactiveSessionItemWithDate(_ p1: String) -> String { return VectorL10n.tr("Vector", "user_inactive_session_item_with_date", p1) } + /// All sessions + public static var userOtherSessionFilterMenuAll: String { + return VectorL10n.tr("Vector", "user_other_session_filter_menu_all") + } + /// Inactive + public static var userOtherSessionFilterMenuInactive: String { + return VectorL10n.tr("Vector", "user_other_session_filter_menu_inactive") + } + /// Unverified + public static var userOtherSessionFilterMenuUnverified: String { + return VectorL10n.tr("Vector", "user_other_session_filter_menu_unverified") + } + /// Verified + public static var userOtherSessionFilterMenuVerified: String { + return VectorL10n.tr("Vector", "user_other_session_filter_menu_verified") + } /// Security recommendation public static var userOtherSessionSecurityRecommendationTitle: String { return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title") @@ -8635,6 +8651,10 @@ public class VectorL10n: NSObject { public static var userOtherSessionUnverifiedSessionsHeaderSubtitle: String { return VectorL10n.tr("Vector", "user_other_session_unverified_sessions_header_subtitle") } + /// For best security, sign out from any session that you don’t recognize or use anymore. + public static var userOtherSessionVerifiedSessionsHeaderSubtitle: String { + return VectorL10n.tr("Vector", "user_other_session_verified_sessions_header_subtitle") + } /// Name public static var userSessionDetailsApplicationName: String { return VectorL10n.tr("Vector", "user_session_details_application_name") diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift index 09315e49f..f6d9205c9 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift @@ -19,19 +19,22 @@ import Foundation enum OtherUserSessionsFilter: Identifiable, Equatable, CaseIterable { var id: Self { self } case all - case inactive + case verified case unverified + case inactive } extension OtherUserSessionsFilter { var menuLocalizedName: String { switch self { case .all: - return "All sessions" - case .inactive: - return "Inactive" + return VectorL10n.userOtherSessionFilterMenuAll + case .verified: + return VectorL10n.userOtherSessionFilterMenuVerified case .unverified: - return "Unverified" + return VectorL10n.userOtherSessionFilterMenuUnverified + case .inactive: + return VectorL10n.userOtherSessionFilterMenuInactive } } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift index 34f54b604..849eb4b1f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift @@ -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) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index d5411be5c..2addafa8d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -71,6 +71,8 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi return sessionInfos.filter { !$0.isActive } case .unverified: return sessionInfos.filter { !$0.isVerified } + case .verified: + return sessionInfos.filter { $0.isVerified } } } @@ -81,13 +83,17 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi 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) } } } From 6225aab76b66a177ed0560cbee5e448dd3844c43 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 11 Oct 2022 14:54:09 +0300 Subject: [PATCH 03/12] Clear filter button --- Riot/Assets/en.lproj/Vector.strings | 5 +++ Riot/Generated/Strings.swift | 16 ++++++++ .../UserOtherSessionsViewModelTests.swift | 19 +++++---- .../UserOtherSessionsModels.swift | 2 + .../UserOtherSessionsViewModel.swift | 37 +++++++++++++---- .../View/UserOtherSessions.swift | 40 +++++++++++++++++-- 6 files changed, 102 insertions(+), 17 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c81878669..c56cca72b 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2437,6 +2437,11 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_filter_menu_verified" = "Verified"; "user_other_session_filter_menu_unverified" = "Unverified"; "user_other_session_filter_menu_inactive" = "Inactive"; + +"user_other_session_no_inactive_sessions" = "No inactive sessions found."; +"user_other_session_no_verified_sessions" = "No verified sessions found."; +"user_other_session_no_unverified_sessions" = "No unverified sessions found."; +"user_other_session_clear_filter" = "Clear filter"; // First item is client name and second item is session display name "user_session_name" = "%@: %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 16022e641..0f948e1f0 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8623,6 +8623,10 @@ public class VectorL10n: NSObject { public static func userInactiveSessionItemWithDate(_ p1: String) -> String { return VectorL10n.tr("Vector", "user_inactive_session_item_with_date", p1) } + /// Clear filter + public static var userOtherSessionClearFilter: String { + return VectorL10n.tr("Vector", "user_other_session_clear_filter") + } /// All sessions public static var userOtherSessionFilterMenuAll: String { return VectorL10n.tr("Vector", "user_other_session_filter_menu_all") @@ -8639,6 +8643,18 @@ public class VectorL10n: NSObject { public static var userOtherSessionFilterMenuVerified: String { return VectorL10n.tr("Vector", "user_other_session_filter_menu_verified") } + /// No inactive sessions found. + public static var userOtherSessionNoInactiveSessions: String { + return VectorL10n.tr("Vector", "user_other_session_no_inactive_sessions") + } + /// No unverified sessions found. + public static var userOtherSessionNoUnverifiedSessions: String { + return VectorL10n.tr("Vector", "user_other_session_no_unverified_sessions") + } + /// No verified sessions found. + public static var userOtherSessionNoVerifiedSessions: String { + return VectorL10n.tr("Vector", "user_other_session_no_verified_sessions") + } /// Security recommendation public static var userOtherSessionSecurityRecommendationTitle: String { return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title") diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index 5e6f15014..5577e7750 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -35,16 +35,18 @@ class UserOtherSessionsViewModelTests: XCTestCase { } func test_whenModelCreated_withInactiveFilter_viewStateIsCorrect() { - let sessionInfos = [createUserSessionInfo(sessionId: "session 1"), createUserSessionInfo(sessionId: "session 2")] + let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isActive: false), + createUserSessionInfo(sessionId: "session 2", isActive: false)] let sut = UserOtherSessionsViewModel(sessionInfos: sessionInfos, filter: .inactive, title: "Title") - let expectedHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle, + let expectedHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userOtherSessionFilterMenuInactive, subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo, iconName: Asset.Images.userOtherSessionsInactive.name) let expectedItems = sessionInfos.filter { !$0.isActive }.asViewData() - let expectedState = UserOtherSessionsViewState(title: "Title", + let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .inactive), + title: "Title", sections: [.sessionItems(header: expectedHeader, items: expectedItems)]) XCTAssertEqual(sut.state, expectedState) } @@ -59,12 +61,15 @@ class UserOtherSessionsViewModelTests: XCTestCase { subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo, iconName: nil) let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData() - let expectedState = UserOtherSessionsViewState(title: "Title", + let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .all), + title: "Title", sections: [.sessionItems(header: expectedHeader, items: expectedItems)]) XCTAssertEqual(sut.state, expectedState) } - private func createUserSessionInfo(sessionId: String) -> UserSessionInfo { + private func createUserSessionInfo(sessionId: String, + isActive: Bool = true, + isCurrent: Bool = false) -> UserSessionInfo { UserSessionInfo(id: sessionId, name: "iOS", deviceType: .mobile, @@ -79,7 +84,7 @@ class UserOtherSessionsViewModelTests: XCTestCase { lastSeenIPLocation: nil, clientName: nil, clientVersion: nil, - isActive: true, - isCurrent: true) + isActive: isActive, + isCurrent: isCurrent) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index 50b15b054..5d6d06c9e 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -46,9 +46,11 @@ enum UserOtherSessionsSection: Hashable, Identifiable { } case sessionItems(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) + case emptySessionItems(header: UserOtherSessionsHeaderViewData, title: String) } enum UserOtherSessionsViewAction { case userOtherSessionSelected(sessionId: String) case filerWasChanged + case clearFilter } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 2addafa8d..bef8286e1 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -29,7 +29,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi super.init(initialViewState: UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: filter), title: title, sections: [])) - updateViewState(filter: filter) + updateViewState() } // MARK: - Public @@ -43,16 +43,25 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi } completion?(.showUserSessionOverview(sessionInfo: session)) case .filerWasChanged: - updateViewState(filter: state.bindings.filter) + updateViewState() + case .clearFilter: + state.bindings.filter = .all + updateViewState() } } // MARK: - Private - private func updateViewState(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] { @@ -72,7 +81,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi case .unverified: return sessionInfos.filter { !$0.isVerified } case .verified: - return sessionInfos.filter { $0.isVerified } + return sessionInfos.filter(\.isVerified) } } @@ -96,4 +105,18 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi iconName: Asset.Images.userOtherSessionsVerified.name) } } + + private func noSessionsTitle(filter: OtherUserSessionsFilter) -> 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 + } + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 6ed50f7e6..91f64398f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -27,6 +27,8 @@ 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) } } } @@ -63,11 +65,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 From 0a01a1ca9e18c0d15ee3d461c2c2fff04f973b79 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 11 Oct 2022 17:19:32 +0300 Subject: [PATCH 04/12] UI and unit tests --- .../MockUserOtherSessionsScreenState.swift | 42 ++++++- .../Test/UI/UserOtherSessionsUITests.swift | 7 ++ .../UserOtherSessionsViewModelTests.swift | 112 ++++++++++++++---- 3 files changed, 140 insertions(+), 21 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift index 1b4f8a48c..438734053 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift @@ -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, + isVerified: true, + 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, + isVerified: true, + 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", diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift index 849eb4b1f..1273f32d5 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift @@ -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) + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index 5577e7750..b20c18728 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -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 @@ -37,43 +52,100 @@ class UserOtherSessionsViewModelTests: XCTestCase { func test_whenModelCreated_withInactiveFilter_viewStateIsCorrect() { let sessionInfos = [createUserSessionInfo(sessionId: "session 1", isActive: false), createUserSessionInfo(sessionId: "session 2", isActive: false)] - let sut = UserOtherSessionsViewModel(sessionInfos: sessionInfos, - filter: .inactive, - title: "Title") + let sut = createSUT(sessionInfos: sessionInfos, filter: .inactive) - let expectedHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userOtherSessionFilterMenuInactive, - subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo, - iconName: Asset.Images.userOtherSessionsInactive.name) let expectedItems = sessionInfos.filter { !$0.isActive }.asViewData() let expectedState = UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: .inactive), title: "Title", - sections: [.sessionItems(header: expectedHeader, items: expectedItems)]) + 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(bindings: UserOtherSessionsBindings(filter: .all), title: "Title", - sections: [.sessionItems(header: expectedHeader, items: expectedItems)]) + sections: [.sessionItems(header: allSectionHeader, items: expectedItems)]) XCTAssertEqual(sut.state, expectedState) } + 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: OtherUserSessionsFilter, + 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, - isVerified: false, + isVerified: isVerified, lastSeenIP: "10.0.0.10", lastSeenTimestamp: Date().timeIntervalSince1970 - 100, applicationName: nil, From c031dbb4de906f13e33e9bab21e91b3cbcab3e93 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 09:30:41 +0300 Subject: [PATCH 05/12] Renamed OtherUserSessionsFilter to UserOtherSessionsFilter --- .../Coordinator/UserSessionsFlowCoordinator.swift | 4 ++-- .../Coordinator/UserOtherSessionsCoordinator.swift | 2 +- .../Test/Unit/UserOtherSessionsViewModelTests.swift | 2 +- ...sionsFilter.swift => UserOtherSessionsFilter.swift} | 4 ++-- .../UserOtherSessions/UserOtherSessionsModels.swift | 2 +- .../UserOtherSessions/UserOtherSessionsViewModel.swift | 10 +++++----- .../UserOtherSessions/View/UserOtherSessions.swift | 2 +- .../Coordinator/UserSessionsOverviewCoordinator.swift | 2 +- .../UserSessionsOverviewModels.swift | 4 ++-- .../UserSessionsOverviewViewModel.swift | 2 +- 10 files changed, 17 insertions(+), 17 deletions(-) rename RiotSwiftUI/Modules/UserSessions/UserOtherSessions/{OtherUserSessionsFilter.swift => UserOtherSessionsFilter.swift} (91%) diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 06d13ebcb..0361832b4 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -127,7 +127,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, @@ -143,7 +143,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, diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift index 607a87aa9..82dcd78d7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift @@ -19,7 +19,7 @@ import SwiftUI struct UserOtherSessionsCoordinatorParameters { let sessionInfos: [UserSessionInfo] - let filter: OtherUserSessionsFilter + let filter: UserOtherSessionsFilter let title: String } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index b20c18728..4e2292b41 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -131,7 +131,7 @@ class UserOtherSessionsViewModelTests: XCTestCase { } private func createSUT(sessionInfos: [UserSessionInfo], - filter: OtherUserSessionsFilter, + filter: UserOtherSessionsFilter, title: String = "Title") -> UserOtherSessionsViewModel { UserOtherSessionsViewModel(sessionInfos: sessionInfos, filter: filter, diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsFilter.swift similarity index 91% rename from RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift rename to RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsFilter.swift index f6d9205c9..9450c4d74 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/OtherUserSessionsFilter.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsFilter.swift @@ -16,7 +16,7 @@ import Foundation -enum OtherUserSessionsFilter: Identifiable, Equatable, CaseIterable { +enum UserOtherSessionsFilter: Identifiable, Equatable, CaseIterable { var id: Self { self } case all case verified @@ -24,7 +24,7 @@ enum OtherUserSessionsFilter: Identifiable, Equatable, CaseIterable { case inactive } -extension OtherUserSessionsFilter { +extension UserOtherSessionsFilter { var menuLocalizedName: String { switch self { case .all: diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index 5d6d06c9e..607d6cf33 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -37,7 +37,7 @@ struct UserOtherSessionsViewState: BindableState, Equatable { } struct UserOtherSessionsBindings: Equatable { - var filter: OtherUserSessionsFilter + var filter: UserOtherSessionsFilter } enum UserOtherSessionsSection: Hashable, Identifiable { diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index bef8286e1..2cfb11abf 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -23,7 +23,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi private let sessionInfos: [UserSessionInfo] init(sessionInfos: [UserSessionInfo], - filter: OtherUserSessionsFilter, + filter: UserOtherSessionsFilter, title: String) { self.sessionInfos = sessionInfos super.init(initialViewState: UserOtherSessionsViewState(bindings: UserOtherSessionsBindings(filter: filter), @@ -64,7 +64,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi } } - 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, @@ -72,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 } @@ -85,7 +85,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi } } - private func createHeaderData(filter: OtherUserSessionsFilter) -> UserOtherSessionsHeaderViewData { + private func createHeaderData(filter: UserOtherSessionsFilter) -> UserOtherSessionsHeaderViewData { switch filter { case .all: return UserOtherSessionsHeaderViewData(title: nil, @@ -106,7 +106,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi } } - private func noSessionsTitle(filter: OtherUserSessionsFilter) -> String { + private func noSessionsTitle(filter: UserOtherSessionsFilter) -> String { switch filter { case .all: assertionFailure("The view is not intended to be displayed without any session") diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 91f64398f..77b84d12c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -39,7 +39,7 @@ struct UserOtherSessions: View { ToolbarItem(placement: .navigationBarTrailing) { Menu { Picker("Filter menu", selection: $viewModel.filter) { - ForEach(OtherUserSessionsFilter.allCases) { filter in + ForEach(UserOtherSessionsFilter.allCases) { filter in Text(filter.menuLocalizedName).tag(filter) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index c3117f9ba..1f01f5d3f 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -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)) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index b8fadf8ee..b2a133280 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -22,14 +22,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) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index 3855b61e2..eebfc0a94 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -107,7 +107,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess } } - private func showSessions(filteredBy filter: OtherUserSessionsFilter) { + private func showSessions(filteredBy filter: UserOtherSessionsFilter) { completion?(.showOtherSessions(sessionInfos: userSessionsOverviewService.sessionInfos, filter: filter)) } From 4203b42533b54737d525694fa9a4ca21247c7e23 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 09:48:09 +0300 Subject: [PATCH 06/12] Changelog --- changelog.d/6838.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6838.wip diff --git a/changelog.d/6838.wip b/changelog.d/6838.wip new file mode 100644 index 000000000..15c28e09a --- /dev/null +++ b/changelog.d/6838.wip @@ -0,0 +1 @@ +Device Manager: Filter sessions. From 5b78cf37454a89c29ea078c42d9dcbcb396ff5de Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 11:00:38 +0300 Subject: [PATCH 07/12] Formating --- .../UserOtherSessions/UserOtherSessionsViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index a8f11a6b0..6e41aab12 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -81,7 +81,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi case .unverified: return sessionInfos.filter { $0.verificationState != .verified } case .verified: - return sessionInfos.filter {$0.verificationState == .verified} + return sessionInfos.filter { $0.verificationState == .verified } } } From f755f49c4dc85b1c3ace77de65851cf302681ecd Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 13:11:09 +0300 Subject: [PATCH 08/12] Update Riot/Assets/en.lproj/Vector.strings Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- Riot/Assets/en.lproj/Vector.strings | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 8685c71be..12f833d00 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2439,10 +2439,10 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_unverified_current_session_details" = "%@ · Your current session"; "user_other_session_verified_sessions_header_subtitle" = "For best security, sign out from any session that you don’t recognize or use anymore."; -"user_other_session_filter_menu_all" = "All sessions"; -"user_other_session_filter_menu_verified" = "Verified"; -"user_other_session_filter_menu_unverified" = "Unverified"; -"user_other_session_filter_menu_inactive" = "Inactive"; +"user_other_session_filter_menu_all" = "All sessions"; +"user_other_session_filter_menu_verified" = "Verified"; +"user_other_session_filter_menu_unverified" = "Unverified"; +"user_other_session_filter_menu_inactive" = "Inactive"; "user_other_session_no_inactive_sessions" = "No inactive sessions found."; "user_other_session_no_verified_sessions" = "No verified sessions found."; From 2d0a38ae1ba15f9173afe4fa5ebd60c0c6911586 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 13:11:43 +0300 Subject: [PATCH 09/12] Update RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> --- .../UserOtherSessions/UserOtherSessionsModels.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index 607d6cf33..b81444b92 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -51,6 +51,6 @@ enum UserOtherSessionsSection: Hashable, Identifiable { enum UserOtherSessionsViewAction { case userOtherSessionSelected(sessionId: String) - case filerWasChanged + case filterWasChanged case clearFilter } From 20c2159145702a767c9a13c7c23dc78206720ff7 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 13:16:19 +0300 Subject: [PATCH 10/12] Single scale image --- .../Contents.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json index 54a3ea24f..89113e4ef 100644 --- a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_filter_selected.imageset/Contents.json @@ -2,16 +2,7 @@ "images" : [ { "filename" : "user_other_sessions_filter_selected.svg", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "idiom" : "universal" } ], "info" : { From 5329a03477adb953b60950968ab6cd49836889ca Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 13:18:33 +0300 Subject: [PATCH 11/12] Renamed filerWasChanged to filterWasChanged --- .../UserOtherSessions/UserOtherSessionsViewModel.swift | 2 +- .../UserSessions/UserOtherSessions/View/UserOtherSessions.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 6e41aab12..9bad552d8 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -42,7 +42,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi return } completion?(.showUserSessionOverview(sessionInfo: session)) - case .filerWasChanged: + case .filterWasChanged: updateViewState() case .clearFilter: state.bindings.filter = .all diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 77b84d12c..c4d183b02 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -45,7 +45,7 @@ struct UserOtherSessions: View { } .labelsHidden() .onChange(of: viewModel.filter) { _ in - viewModel.send(viewAction: .filerWasChanged) + viewModel.send(viewAction: .filterWasChanged) } } label: { Image(viewModel.filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name) From 4b21f7276dba009291660c9618ee13356a5fcdbb Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 12 Oct 2022 14:34:26 +0300 Subject: [PATCH 12/12] Added accessibility label to filter menu --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../UserOtherSessions/View/UserOtherSessions.swift | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 12f833d00..45b00489a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2439,6 +2439,7 @@ To enable access, tap Settings> Location and select Always"; "user_other_session_unverified_current_session_details" = "%@ · Your current session"; "user_other_session_verified_sessions_header_subtitle" = "For best security, sign out from any session that you don’t recognize or use anymore."; +"user_other_session_filter" = "Filter"; "user_other_session_filter_menu_all" = "All sessions"; "user_other_session_filter_menu_verified" = "Verified"; "user_other_session_filter_menu_unverified" = "Unverified"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 4f323f56c..6d4281200 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8639,6 +8639,10 @@ public class VectorL10n: NSObject { public static var userOtherSessionClearFilter: String { return VectorL10n.tr("Vector", "user_other_session_clear_filter") } + /// Filter + public static var userOtherSessionFilter: String { + return VectorL10n.tr("Vector", "user_other_session_filter") + } /// All sessions public static var userOtherSessionFilterMenuAll: String { return VectorL10n.tr("Vector", "user_other_session_filter_menu_all") diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index c4d183b02..9b64b201b 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -38,7 +38,7 @@ struct UserOtherSessions: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Menu { - Picker("Filter menu", selection: $viewModel.filter) { + Picker("", selection: $viewModel.filter) { ForEach(UserOtherSessionsFilter.allCases) { filter in Text(filter.menuLocalizedName).tag(filter) } @@ -50,6 +50,7 @@ struct UserOtherSessions: View { } label: { Image(viewModel.filter == .all ? Asset.Images.userOtherSessionsFilter.name : Asset.Images.userOtherSessionsFilterSelected.name) } + .accessibilityLabel(VectorL10n.userOtherSessionFilter) } } }