From 0b85beb0f4d86b33662669ed735f98ef270aa40b Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Fri, 30 Sep 2022 16:34:41 +0300 Subject: [PATCH 01/11] User other sessions screen template --- .../Contents.json | 12 +++ .../user_other_sessions_inactive.svg | 3 + Riot/Generated/Images.swift | 1 + .../Common/View/DeviceAvatarViewData.swift | 2 +- .../UserSessionsFlowCoordinator.swift | 24 ++++++ .../UserOtherSessionsCoordinator.swift | 80 +++++++++++++++++ .../MockUserOtherSessionsScreenState.swift | 51 +++++++++++ .../Test/UI/UserOtherSessionsUITests.swift | 22 +++++ .../UserOtherSessionsViewModelTests.swift | 23 +++++ .../UserOtherSessionsModels.swift | 50 +++++++++++ .../UserOtherSessionsViewModel.swift | 86 +++++++++++++++++++ .../UserOtherSessionsViewModelProtocol.swift | 22 +++++ .../View/UserOtherSessions.swift | 66 ++++++++++++++ .../View/UserOtherSessionsHeaderView.swift | 76 ++++++++++++++++ .../UserSessionOverviewViewModelTests.swift | 2 +- .../UserSessionsOverviewCoordinator.swift | 23 ++--- .../UserSessionsOverviewModels.swift | 5 +- .../UserSessionsOverviewViewModel.swift | 13 ++- .../View/UserSessionListItemViewData.swift | 2 +- 19 files changed, 536 insertions(+), 27 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/user_other_sessions_inactive.svg create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModelProtocol.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/Contents.json new file mode 100644 index 0000000000..f16dd6afe8 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_other_sessions_inactive.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/user_other_sessions_inactive.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/user_other_sessions_inactive.svg new file mode 100644 index 0000000000..8603bbc319 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_other_sessions_inactive.imageset/user_other_sessions_inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 9dc652238d..d74ebcee7a 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -105,6 +105,7 @@ 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 userOtherSessionsInactive = ImageAsset(name: "user_other_sessions_inactive") internal static let userSessionUnverified = ImageAsset(name: "user_session_unverified") internal static let userSessionVerified = ImageAsset(name: "user_session_verified") internal static let userSessionsInactive = ImageAsset(name: "user_sessions_inactive") diff --git a/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift index 397e945dc4..ee37fbcd83 100644 --- a/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/View/DeviceAvatarViewData.swift @@ -18,7 +18,7 @@ import Foundation import SwiftUI /// View data for DeviceAvatarView -struct DeviceAvatarViewData { +struct DeviceAvatarViewData: Hashable { let deviceType: DeviceType let isVerified: Bool? } diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 288ce2bf4a..4fde52538c 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -55,6 +55,9 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { switch result { case let .openSessionOverview(session: session): self.openSessionOverview(session: session) + case let .openOtherSessions(sessions: sessions, filter: filter): + let title = filter == .all ? "Other sessions" : "Security recommendation" + self.openOtherSessions(sessions: sessions, filterBy: filter, title: title) } } return coordinator @@ -87,6 +90,27 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { return UserSessionOverviewCoordinator(parameters: parameters) } + private func openOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) { + let coordinator = createOtherSessionsCoordinator(sessions: sessions, + filterBy: filter, + title: title) + coordinator.completion = { [weak self] _ in +// guard let self = self else { return } +// switch result { +// case let .openSessionDetails(session: session): +// self.openSessionDetails(session: session) +// } + } + pushScreen(with: coordinator) + } + + private func createOtherSessionsCoordinator(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) -> UserOtherSessionsCoordinator { + let parameters = UserOtherSessionsCoordinatorParameters(sessions: sessions, + filter: filter, + title: title) + return UserOtherSessionsCoordinator(parameters: parameters) + } + // MARK: - Public func start() { diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift new file mode 100644 index 0000000000..bcb9133688 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift @@ -0,0 +1,80 @@ +// +// Copyright 2021 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 CommonKit +import SwiftUI + +struct UserOtherSessionsCoordinatorParameters { + let sessions: [UserSessionInfo] + let filter: OtherUserSessionsFilter + let title: String +} + +final class UserOtherSessionsCoordinator: Coordinator, Presentable { + private let parameters: UserOtherSessionsCoordinatorParameters + private let userOtherSessionsHostingController: UIViewController + private var userOtherSessionsViewModel: UserOtherSessionsViewModelProtocol + + private var indicatorPresenter: UserIndicatorTypePresenterProtocol + private var loadingIndicator: UserIndicator? + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + var completion: ((UserOtherSessionsViewModelResult) -> Void)? + + init(parameters: UserOtherSessionsCoordinatorParameters) { + self.parameters = parameters + + let viewModel = UserOtherSessionsViewModel(sessions: parameters.sessions, + filter: parameters.filter, + title: parameters.title) + let view = UserOtherSessions(viewModel: viewModel.context) + userOtherSessionsViewModel = viewModel + userOtherSessionsHostingController = VectorHostingController(rootView: view) + + indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: userOtherSessionsHostingController) + } + + // MARK: - Public + + func start() { + MXLog.debug("[UserOtherSessionsCoordinator] did start.") + userOtherSessionsViewModel.completion = { [weak self] result in + guard let self = self else { return } + MXLog.debug("[UserOtherSessionsCoordinator] UserOtherSessionsViewModel did complete with result: \(result).") + self.completion?(result) + } + } + + func toPresentable() -> UIViewController { + userOtherSessionsHostingController + } + + // MARK: - Private + + /// Show an activity indicator whilst loading. + /// - Parameters: + /// - label: The label to show on the indicator. + /// - isInteractionBlocking: Whether the indicator should block any user interaction. + private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { + loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) + } + + /// Hide the currently displayed activity indicator. + private func stopLoading() { + loadingIndicator = nil + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift new file mode 100644 index 0000000000..ec16b53699 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift @@ -0,0 +1,51 @@ +// +// Copyright 2021 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 SwiftUI + +/// Using an enum for the screen allows you define the different state cases with +/// the relevant associated data for each case. +enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { + // A case for each state you want to represent + // with specific, minimal associated data that will allow you + // mock that screen. + + case allSessionsEmpty + + /// The associated screen + var screenType: Any.Type { + UserOtherSessions.self + } + + /// A list of screen state definitions + static var allCases: [MockUserOtherSessionsScreenState] { + // Each of the presence statuses + [.allSessionsEmpty] + } + + /// Generate the view struct for the screen state. + var screenView: ([Any], AnyView) { + let viewModel = UserOtherSessionsViewModel(sessions: [], filter: .all, title: "Other sessions") + + // can simulate service and viewModel actions here if needs be. + + return ( + [viewModel], + AnyView(UserOtherSessions(viewModel: viewModel.context)) + ) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift new file mode 100644 index 0000000000..746acd8146 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift @@ -0,0 +1,22 @@ +// +// Copyright 2021 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 RiotSwiftUI +import XCTest + +class UserOtherSessionsUITests: MockScreenTestCase { + +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift new file mode 100644 index 0000000000..dc6ed7f106 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -0,0 +1,23 @@ +// +// Copyright 2021 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 XCTest + +@testable import RiotSwiftUI + +class UserOtherSessionsViewModelTests: XCTestCase { + +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift new file mode 100644 index 0000000000..f74561d23a --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -0,0 +1,50 @@ +// +// Copyright 2021 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 + +// MARK: - Coordinator + + +// MARK: View model + +enum UserOtherSessionsViewModelResult { +} + +// MARK: View + +struct UserOtherSessionsViewState: BindableState { + let title: String + var sections: [UserOtherSessionsSection] +} + +enum UserOtherSessionsSection: Hashable, Identifiable { + var id: Self { + self + } + case sessionItems(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) + case clearFilter +} + +enum UserOtherSessionsViewAction { +} + + +enum OtherUserSessionsFilter { + case all + case inactive + case unverified +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift new file mode 100644 index 0000000000..4bd4bc608c --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -0,0 +1,86 @@ +// +// Copyright 2021 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 SwiftUI + +typealias UserOtherSessionsViewModelType = StateStoreViewModel + +class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol { + var completion: ((UserOtherSessionsViewModelResult) -> Void)? + + init(sessions: [UserSessionInfo], + filter: OtherUserSessionsFilter, + title: String) { + + super.init(initialViewState: UserOtherSessionsViewState(title: title, sections: [])) + updateViewState(sessions: sessions, filter: filter) + } + + // MARK: - Public + + override func process(viewAction: UserOtherSessionsViewAction) { + // switch viewAction { + // case .accept: + // completion?(.accept) + // case .cancel: + // completion?(.cancel) + // case .incrementCount: + // state.count += 1 + // case .decrementCount: + // state.count -= 1 + // } + } + + // MARK: - Private + + private func updateViewState(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter) { + let sectionItems = filterSessions(sessions: sessions, by: filter).asViewData() + let sectionHeader = createHeaderData(filter: filter) + state.sections = [.sessionItems(header: sectionHeader, items: sectionItems)] + } + + private func filterSessions(sessions: [UserSessionInfo], by filter: OtherUserSessionsFilter) -> [UserSessionInfo] { + switch filter { + case .all: + return sessions.filter { !$0.isCurrent } + case .inactive: + return sessions.filter { !$0.isActive } + case .unverified: + return sessions.filter { !$0.isVerified } + } + } + + private func createHeaderData(filter: OtherUserSessionsFilter) -> UserOtherSessionsHeaderViewData { + switch filter { + case .all: + // TODO: + return UserOtherSessionsHeaderViewData(title: nil, + subtitle: "", + iconName: nil) + case .inactive: + return UserOtherSessionsHeaderViewData(title: "Inactive sessions", + subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore. Learn more", + iconName: Asset.Images.userOtherSessionsInactive.name) + case .unverified: + // TODO: + return UserOtherSessionsHeaderViewData(title: nil, + subtitle: "", + iconName: nil) + + } + } +} + diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModelProtocol.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModelProtocol.swift new file mode 100644 index 0000000000..444fe1fc85 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModelProtocol.swift @@ -0,0 +1,22 @@ +// +// Copyright 2021 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 + +protocol UserOtherSessionsViewModelProtocol { + var completion: ((UserOtherSessionsViewModelResult) -> Void)? { get set } + var context: UserOtherSessionsViewModelType.Context { get } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift new file mode 100644 index 0000000000..f2729cf962 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -0,0 +1,66 @@ +// +// Copyright 2021 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 SwiftUI + +struct UserOtherSessions: View { + + @Environment(\.theme) private var theme + + @ObservedObject var viewModel: UserOtherSessionsViewModel.Context + + var body: some View { + ScrollView { + ForEach(viewModel.viewState.sections) { section in + + switch section { + case let .sessionItems(header: header, items: items): + SwiftUI.Section { + LazyVStack(spacing: 0) { + ForEach(items) { viewData in + UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + // viewModel.send(viewAction: .tapUserSession(sessionId)) + }) + } + } + .background(theme.colors.background) + } header: { + UserOtherSessionsHeaderView(viewData: header) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16.0) + .padding(.top, 24.0) + } + case .clearFilter: + //TODO + EmptyView() + } + } + + } + .background(theme.colors.system.ignoresSafeArea()) + .frame(maxHeight: .infinity) + .navigationTitle(viewModel.viewState.title) + } +} + +// MARK: - Previews + +struct UserOtherSessions_Previews: PreviewProvider { + static let stateRenderer = MockUserOtherSessionsScreenState.stateRenderer + static var previews: some View { + stateRenderer.screenGroup() + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift new file mode 100644 index 0000000000..11b3c42478 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift @@ -0,0 +1,76 @@ +// +// 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 SwiftUI + +struct UserOtherSessionsHeaderViewData { + var title: String? + var subtitle: String + var iconName: String? +} + +struct UserOtherSessionsHeaderView: View { + + @Environment(\.theme) private var theme + + let viewData: UserOtherSessionsHeaderViewData + + var body: some View { + HStack (alignment: .top, spacing: 0) { + if let iconName = viewData.iconName { + Image(iconName) + .foregroundColor(.red) + } + VStack(alignment: .leading, spacing: 0, content: { + if let title = viewData.title { + Text(title) + .font(.callout) + .textCase(.uppercase) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.primaryContent) + .padding(.bottom, 9.0) + } + Text(viewData.subtitle) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + .padding(.bottom, 12.0) + }) + .padding(.leading, 16) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + } +} + + +struct UserOtherSessionsHeaderView_Previews: PreviewProvider { + + private static let inactiveSessionViewData = UserOtherSessionsHeaderViewData(title: "Inactive sessions", + subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore. Learn more", + iconName: Asset.Images.userOtherSessionsInactive.name) + + + static var previews: some View { + Group { + UserOtherSessionsHeaderView(viewData: self.inactiveSessionViewData) + .theme(.light) + .preferredColorScheme(.light) + UserOtherSessionsHeaderView(viewData: self.inactiveSessionViewData) + .theme(.dark) + .preferredColorScheme(.dark) + } + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift index a3fb37f5fe..49349bb7f4 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift @@ -44,7 +44,7 @@ class UserSessionOverviewViewModelTests: XCTestCase { sut.process(viewAction: .viewSessionDetails) XCTAssertEqual(modelResult, .showSessionDetails(session: session)) } - + private func createUserSessionInfo() -> UserSessionInfo { UserSessionInfo(id: "session", name: "iOS", diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index ac7521dcbf..2aad0fae17 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -52,16 +52,12 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { MXLog.debug("[UserSessionsOverviewCoordinator] UserSessionsOverviewViewModel did complete with result: \(result).") switch result { - case .showAllUnverifiedSessions: - self.showAllUnverifiedSessions() - case .showAllInactiveSessions: - self.showAllInactiveSessions() + case let .showOtherSessions(sessions: sessions, filter: filter): + self.showOtherSessions(sessions: sessions, filterBy: filter) case .verifyCurrentSession: self.startVerifyCurrentSession() case let .showCurrentSessionOverview(session): self.showCurrentSessionOverview(session: session) - case .showAllOtherSessions: - self.showAllOtherSessions() case let .showUserSessionOverview(session): self.showUserSessionOverview(session: session) } @@ -87,16 +83,12 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { loadingIndicator = nil } - private func showAllUnverifiedSessions() { - // TODO: - } - - private func showAllInactiveSessions() { - // TODO: + private func showOtherSessions(sessions: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) { + completion?(.openOtherSessions(sessions: sessions, filter: filter)) } private func startVerifyCurrentSession() { - // TODO: + // TODO:openSessionOverview } private func showCurrentSessionOverview(session: UserSessionInfo) { @@ -106,8 +98,5 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { private func showUserSessionOverview(session: UserSessionInfo) { completion?(.openSessionOverview(session: session)) } - - private func showAllOtherSessions() { - // TODO: - } + } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift index ce3e56f0a1..45621c9ed5 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift @@ -20,16 +20,15 @@ import Foundation enum UserSessionsOverviewCoordinatorResult { case openSessionOverview(session: UserSessionInfo) + case openOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter) } // MARK: View model enum UserSessionsOverviewViewModelResult { - case showAllUnverifiedSessions - case showAllInactiveSessions + case showOtherSessions(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter) case verifyCurrentSession case showCurrentSessionOverview(session: UserSessionInfo) - case showAllOtherSessions case showUserSessionOverview(session: UserSessionInfo) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index 65fefaf7ed..fb0ef5e77d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -46,11 +46,11 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess } completion?(.showCurrentSessionOverview(session: currentSessionInfo)) case .viewAllUnverifiedSessions: - completion?(.showAllUnverifiedSessions) + showSessions(filteredBy: .unverified) case .viewAllInactiveSessions: - completion?(.showAllInactiveSessions) + showSessions(filteredBy: .inactive) case .viewAllOtherSessions: - completion?(.showAllOtherSessions) + showSessions(filteredBy: .all) case .tapUserSession(let sessionId): guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else { assertionFailure("Missing session info") @@ -91,9 +91,14 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess } } } + + private func showSessions(filteredBy filter: OtherUserSessionsFilter) { + completion?(.showOtherSessions(sessions: userSessionsOverviewService.overviewData.otherSessions, + filter: filter)) + } } -private extension Collection where Element == UserSessionInfo { +extension Collection where Element == UserSessionInfo { func asViewData() -> [UserSessionListItemViewData] { map { UserSessionListItemViewData(session: $0) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift index 56bccbbadc..ad470b305e 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift @@ -17,7 +17,7 @@ import Foundation /// View data for UserSessionListItem -struct UserSessionListItemViewData: Identifiable { +struct UserSessionListItemViewData: Identifiable, Hashable { private static let userSessionNameFormatter = UserSessionNameFormatter() private static let lastActivityDateFormatter = UserSessionLastActivityFormatter() From 899539fbeb48e00a90ab4e81245af5f8cada49d1 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Mon, 3 Oct 2022 15:45:06 +0300 Subject: [PATCH 02/11] Refactored creation of UserSessionListItemViewData, added inactive session icon --- .../Contents.json | 12 +++ ...ser_session_list_item_inactive_session.svg | 3 + ...tiveUserSessionLastActivityFormatter.swift | 33 ++++++++ .../View/UserOtherSessions.swift | 1 - .../View/UserOtherSessionsHeaderView.swift | 14 +++- .../MockUserSessionsOverviewService.swift | 2 +- .../UserSessionsOverviewViewModel.swift | 2 +- .../View/UserSessionListItem.swift | 17 ++-- .../View/UserSessionListItemViewData.swift | 48 +---------- .../UserSessionListItemViewDataFactory.swift | 80 +++++++++++++++++++ 10 files changed, 153 insertions(+), 59 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg create mode 100644 RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift create mode 100644 RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json new file mode 100644 index 0000000000..e3af9f0531 --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "user_session_list_item_inactive_session.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg new file mode 100644 index 0000000000..5aba5a38bb --- /dev/null +++ b/Riot/Assets/Images.xcassets/DeviceManager/user_session_list_item_inactive_session.imageset/user_session_list_item_inactive_session.svg @@ -0,0 +1,3 @@ + + + diff --git a/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift b/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift new file mode 100644 index 0000000000..f777b56d37 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift @@ -0,0 +1,33 @@ +// +// 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 + +class InactiveUserSessionLastActivityFormatter { + private static var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.dateStyle = .medium + dateFormatter.doesRelativeDateFormatting = true + return dateFormatter + }() + + func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String { + let date = Date(timeIntervalSince1970: lastActivityTimestamp) + + return InactiveUserSessionLastActivityFormatter.dateFormatter.string(from: date) + } +} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index f2729cf962..4d7293bc72 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -40,7 +40,6 @@ struct UserOtherSessions: View { } header: { UserOtherSessionsHeaderView(viewData: header) .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16.0) .padding(.top, 24.0) } case .clearFilter: diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift index 11b3c42478..ac8f612d90 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift @@ -16,7 +16,7 @@ import SwiftUI -struct UserOtherSessionsHeaderViewData { +struct UserOtherSessionsHeaderViewData: Hashable { var title: String? var subtitle: String var iconName: String? @@ -24,6 +24,10 @@ struct UserOtherSessionsHeaderViewData { struct UserOtherSessionsHeaderView: View { + private var backgroundShape: RoundedRectangle { + RoundedRectangle(cornerRadius: 8) + } + @Environment(\.theme) private var theme let viewData: UserOtherSessionsHeaderViewData @@ -33,13 +37,15 @@ struct UserOtherSessionsHeaderView: View { if let iconName = viewData.iconName { Image(iconName) .foregroundColor(.red) + .frame(width: 40, height: 40) + .background(theme.colors.background) + .clipShape(backgroundShape) + .shapedBorder(color: theme.colors.quinaryContent, borderWidth: 1.0, shape: backgroundShape) } VStack(alignment: .leading, spacing: 0, content: { if let title = viewData.title { Text(title) - .font(.callout) - .textCase(.uppercase) - .font(theme.fonts.footnote) + .font(theme.fonts.calloutSB) .foregroundColor(theme.colors.primaryContent) .padding(.bottom, 9.0) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift index d94dbe6c99..319092ef8d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift @@ -48,7 +48,7 @@ class MockUserSessionsOverviewService: UserSessionsOverviewServiceProtocol { deviceType: .desktop, isVerified: true, lastSeenIP: "1.0.0.1", - lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000, + lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000, isActive: false, isCurrent: false), UserSessionInfo(id: "2", diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift index fb0ef5e77d..53ec3289bc 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewViewModel.swift @@ -100,6 +100,6 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess extension Collection where Element == UserSessionInfo { func asViewData() -> [UserSessionListItemViewData] { - map { UserSessionListItemViewData(session: $0) } + map { UserSessionListItemViewDataFactory().create(from: $0)} } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift index 51a698541e..001b658b5d 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItem.swift @@ -42,11 +42,16 @@ struct UserSessionListItem: View { .font(theme.fonts.bodySB) .foregroundColor(theme.colors.primaryContent) .multilineTextAlignment(.leading) - - Text(viewData.sessionDetails) - .font(theme.fonts.caption1) - .foregroundColor(theme.colors.secondaryContent) - .multilineTextAlignment(.leading) + HStack { + if let sessionDetailsIcon = viewData.sessionDetailsIcon { + Image(sessionDetailsIcon) + .padding(.leading, 2) + } + Text(viewData.sessionDetails) + .font(theme.fonts.caption1) + .foregroundColor(theme.colors.secondaryContent) + .multilineTextAlignment(.leading) + } } } .frame(maxWidth: .infinity, alignment: .leading) @@ -69,7 +74,7 @@ struct UserSessionListPreview: View { var body: some View { VStack(alignment: .leading, spacing: 0) { ForEach(userSessionsOverviewService.overviewData.otherSessions) { userSessionInfo in - let viewData = UserSessionListItemViewData(session: userSessionInfo) + let viewData = UserSessionListItemViewDataFactory().create(from: userSessionInfo) UserSessionListItem(viewData: viewData, onBackgroundTap: { _ in diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift index ad470b305e..f050def9b7 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift @@ -18,61 +18,17 @@ import Foundation /// View data for UserSessionListItem struct UserSessionListItemViewData: Identifiable, Hashable { - private static let userSessionNameFormatter = UserSessionNameFormatter() - private static let lastActivityDateFormatter = UserSessionLastActivityFormatter() - var id: String { sessionId } let sessionId: String - + let sessionName: String let sessionDetails: String let deviceAvatarViewData: DeviceAvatarViewData - init(sessionId: String, - sessionDisplayName: String?, - deviceType: DeviceType, - isVerified: Bool, - lastActivityDate: TimeInterval?) { - self.sessionId = sessionId - sessionName = Self.userSessionNameFormatter.sessionName(deviceType: deviceType, sessionDisplayName: sessionDisplayName) - sessionDetails = Self.buildSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) - deviceAvatarViewData = DeviceAvatarViewData(deviceType: deviceType, isVerified: isVerified) - } - - // MARK: - Private - - private static func buildSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?) -> String { - let sessionDetailsString: String - - let sessionStatusText = isVerified ? VectorL10n.userSessionVerifiedShort : VectorL10n.userSessionUnverifiedShort - - var lastActivityDateString: String? - - if let lastActivityDate = lastActivityDate { - lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate) - } - - if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false { - sessionDetailsString = VectorL10n.userSessionItemDetails(sessionStatusText, lastActivityDateString) - } else { - sessionDetailsString = sessionStatusText - } - - return sessionDetailsString - } -} - -extension UserSessionListItemViewData { - init(session: UserSessionInfo) { - self.init(sessionId: session.id, - sessionDisplayName: session.name, - deviceType: session.deviceType, - isVerified: session.isVerified, - lastActivityDate: session.lastSeenTimestamp) - } + let sessionDetailsIcon: String? } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift new file mode 100644 index 0000000000..2e6bb38a49 --- /dev/null +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift @@ -0,0 +1,80 @@ +// +// 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 + +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 sessionDetails = buildSessionDetails(isVerified: session.isVerified, + lastActivityDate: session.lastSeenTimestamp, + isActive: session.isActive) + let deviceAvatarViewData = DeviceAvatarViewData(deviceType: session.deviceType, + isVerified: session.isVerified) + return UserSessionListItemViewData(sessionId: session.id, + sessionName: sessionName, + sessionDetails: sessionDetails, + deviceAvatarViewData: deviceAvatarViewData, + sessionDetailsIcon: getSessionDetailsIcon(isActive: session.isActive)) + } + + private func buildSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?, isActive: Bool) -> String { + if isActive { + return activeSessionDetails(isVerified: isVerified, lastActivityDate: lastActivityDate) + } else { + return inactiveSessionDetails(lastActivityDate: lastActivityDate) + } + } + + private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String { + if let lastActivityDate = lastActivityDate { + let lastActivityDateString = Self.inactiveSessionDateFormatter.lastActivityDateString(from: lastActivityDate) + return "Inactive for 90+ days (\(lastActivityDateString))" + } + return "Inactive for 90+ days" + } + + private func activeSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?) -> String { + let sessionDetailsString: String + + let sessionStatusText = isVerified ? VectorL10n.userSessionVerifiedShort : VectorL10n.userSessionUnverifiedShort + + var lastActivityDateString: String? + + if let lastActivityDate = lastActivityDate { + lastActivityDateString = Self.lastActivityDateFormatter.lastActivityDateString(from: lastActivityDate) + } + + if let lastActivityDateString = lastActivityDateString, lastActivityDateString.isEmpty == false { + sessionDetailsString = VectorL10n.userSessionItemDetails(sessionStatusText, lastActivityDateString) + } else { + sessionDetailsString = sessionStatusText + } + + return sessionDetailsString + } + + private func getSessionDetailsIcon(isActive: Bool) -> String? { + isActive ? nil : Asset.Images.userSessionListItemInactiveSession.name + } + +} From 70f6badf08a1c6c8ffb8dce330ffcc835dd68181 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 09:38:53 +0300 Subject: [PATCH 03/11] Cleanup --- Riot/Generated/Images.swift | 1 + .../Modules/Common/Mock/MockAppScreens.swift | 1 + .../UserSessionsFlowCoordinator.swift | 12 +++--- .../UserOtherSessionsCoordinator.swift | 7 +++- .../MockUserOtherSessionsScreenState.swift | 42 +++++++++++++++++-- .../UserOtherSessionsModels.swift | 8 ++-- .../UserOtherSessionsViewModel.swift | 23 +++++----- .../View/UserOtherSessions.swift | 41 +++++++++--------- .../View/UserOtherSessionsHeaderView.swift | 6 +-- 9 files changed, 93 insertions(+), 48 deletions(-) diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index d74ebcee7a..48af01b0cd 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -106,6 +106,7 @@ internal class Asset: NSObject { internal static let deviceTypeUnknown = ImageAsset(name: "device_type_unknown") internal static let deviceTypeWeb = ImageAsset(name: "device_type_web") internal static let userOtherSessionsInactive = ImageAsset(name: "user_other_sessions_inactive") + 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") internal static let userSessionsInactive = ImageAsset(name: "user_sessions_inactive") diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index 05cc502348..3deb51ccbb 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -19,6 +19,7 @@ import Foundation /// The static list of mocked screens in RiotSwiftUI enum MockAppScreens { static let appScreens: [MockScreenState.Type] = [ + MockUserOtherSessionsScreenState.self, MockUserSessionsOverviewScreenState.self, MockUserSessionDetailsScreenState.self, MockUserSessionOverviewScreenState.self, diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 4fde52538c..48b205dc99 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -94,12 +94,12 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { let coordinator = createOtherSessionsCoordinator(sessions: sessions, filterBy: filter, title: title) - coordinator.completion = { [weak self] _ in -// guard let self = self else { return } -// switch result { -// case let .openSessionDetails(session: session): -// self.openSessionDetails(session: session) -// } + coordinator.completion = { [weak self] result in + guard let self = self else { return } + switch result { + case let .openSessionDetails(session: session): + self.openSessionDetails(session: session) + } } pushScreen(with: coordinator) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift index bcb9133688..1216989f68 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift @@ -33,7 +33,7 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable { // Must be used only internally var childCoordinators: [Coordinator] = [] - var completion: ((UserOtherSessionsViewModelResult) -> Void)? + var completion: ((UserOtherSessionsCoordinatorResult) -> Void)? init(parameters: UserOtherSessionsCoordinatorParameters) { self.parameters = parameters @@ -54,8 +54,11 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable { MXLog.debug("[UserOtherSessionsCoordinator] did start.") userOtherSessionsViewModel.completion = { [weak self] result in guard let self = self else { return } + switch result { + case let .showUserSessionOverview(session: session): + self.completion?(.openSessionDetails(session: session)) + } MXLog.debug("[UserOtherSessionsCoordinator] UserOtherSessionsViewModel did complete with result: \(result).") - self.completion?(result) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift index ec16b53699..878d0e5d67 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift @@ -24,7 +24,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { // with specific, minimal associated data that will allow you // mock that screen. - case allSessionsEmpty + case inactiveSessions /// The associated screen var screenType: Any.Type { @@ -34,13 +34,14 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { /// A list of screen state definitions static var allCases: [MockUserOtherSessionsScreenState] { // Each of the presence statuses - [.allSessionsEmpty] + [.inactiveSessions] } /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let viewModel = UserOtherSessionsViewModel(sessions: [], filter: .all, title: "Other sessions") + let viewModel = UserOtherSessionsViewModel(sessions: inactiveSessions(), filter: .inactive, title: "Security recommendation") + // can simulate service and viewModel actions here if needs be. return ( @@ -48,4 +49,39 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { AnyView(UserOtherSessions(viewModel: viewModel.context)) ) } + + private func inactiveSessions() -> [UserSessionInfo] { + [UserSessionInfo(id: "alice", + name: "iOS", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: nil, + isActive: false, + isCurrent: true), + UserSessionInfo(id: "1", + name: "macOS", + deviceType: .desktop, + isVerified: true, + lastSeenIP: "1.0.0.1", + lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000, + isActive: false, + isCurrent: false), + UserSessionInfo(id: "2", + name: "Firefox on Windows", + deviceType: .web, + isVerified: true, + lastSeenIP: "2.0.0.2", + lastSeenTimestamp: Date().timeIntervalSince1970 - 9000000, + isActive: false, + isCurrent: false), + UserSessionInfo(id: "3", + name: "Android", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "3.0.0.3", + lastSeenTimestamp: Date().timeIntervalSince1970 - 10000000, + isActive: false, + isCurrent: false)] + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index f74561d23a..15ef0e88ed 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -17,11 +17,14 @@ import Foundation // MARK: - Coordinator - +enum UserOtherSessionsCoordinatorResult { + case openSessionDetails(session: UserSessionInfo) +} // MARK: View model enum UserOtherSessionsViewModelResult { + case showUserSessionOverview(session: UserSessionInfo) } // MARK: View @@ -36,13 +39,12 @@ enum UserOtherSessionsSection: Hashable, Identifiable { self } case sessionItems(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) - case clearFilter } enum UserOtherSessionsViewAction { + case userOtherSessionSelected(sessionId: String) } - enum OtherUserSessionsFilter { case all case inactive diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 4bd4bc608c..339bc01ed1 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -20,11 +20,12 @@ typealias UserOtherSessionsViewModelType = StateStoreViewModel Void)? + private let sessions: [UserSessionInfo] init(sessions: [UserSessionInfo], filter: OtherUserSessionsFilter, title: String) { - + self.sessions = sessions super.init(initialViewState: UserOtherSessionsViewState(title: title, sections: [])) updateViewState(sessions: sessions, filter: filter) } @@ -32,16 +33,14 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi // MARK: - Public override func process(viewAction: UserOtherSessionsViewAction) { - // switch viewAction { - // case .accept: - // completion?(.accept) - // case .cancel: - // completion?(.cancel) - // case .incrementCount: - // state.count += 1 - // case .decrementCount: - // state.count -= 1 - // } + switch viewAction { + case let .userOtherSessionSelected(sessionId: sessionId): + guard let session = sessions.first(where: {$0.id == sessionId}) else { + assertionFailure("Shouldn't happen, session should be present in the array.") + return + } + completion?(.showUserSessionOverview(session: session)) + } } // MARK: - Private @@ -72,7 +71,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi iconName: nil) case .inactive: return UserOtherSessionsHeaderViewData(title: "Inactive sessions", - subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore. Learn more", + subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore.", iconName: Asset.Images.userOtherSessionsInactive.name) case .unverified: // TODO: diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 4d7293bc72..07207e1d99 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -25,26 +25,9 @@ struct UserOtherSessions: View { var body: some View { ScrollView { ForEach(viewModel.viewState.sections) { section in - switch section { case let .sessionItems(header: header, items: items): - SwiftUI.Section { - LazyVStack(spacing: 0) { - ForEach(items) { viewData in - UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in - // viewModel.send(viewAction: .tapUserSession(sessionId)) - }) - } - } - .background(theme.colors.background) - } header: { - UserOtherSessionsHeaderView(viewData: header) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 24.0) - } - case .clearFilter: - //TODO - EmptyView() + createSessionItemsSection(header: header, items: items) } } @@ -53,13 +36,33 @@ struct UserOtherSessions: View { .frame(maxHeight: .infinity) .navigationTitle(viewModel.viewState.title) } + + private func createSessionItemsSection(header: UserOtherSessionsHeaderViewData, items: [UserSessionListItemViewData]) -> some View { + SwiftUI.Section { + LazyVStack(spacing: 0) { + ForEach(items) { viewData in + UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in + viewModel.send(viewAction: .userOtherSessionSelected(sessionId: sessionId)) + }) + } + } + .background(theme.colors.background) + } header: { + UserOtherSessionsHeaderView(viewData: header) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 24.0) + } + } } // MARK: - Previews struct UserOtherSessions_Previews: PreviewProvider { + static let stateRenderer = MockUserOtherSessionsScreenState.stateRenderer + static var previews: some View { - stateRenderer.screenGroup() + stateRenderer.screenGroup(addNavigation: true).theme(.light).preferredColorScheme(.light) + stateRenderer.screenGroup(addNavigation: true).theme(.dark).preferredColorScheme(.dark) } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift index ac8f612d90..e436142255 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift @@ -47,12 +47,12 @@ struct UserOtherSessionsHeaderView: View { Text(title) .font(theme.fonts.calloutSB) .foregroundColor(theme.colors.primaryContent) - .padding(.bottom, 9.0) + .padding(.vertical, 9.0) } Text(viewData.subtitle) .font(theme.fonts.footnote) .foregroundColor(theme.colors.secondaryContent) - .padding(.bottom, 12.0) + .padding(.bottom, 20.0) }) .padding(.leading, 16) } @@ -65,7 +65,7 @@ struct UserOtherSessionsHeaderView: View { struct UserOtherSessionsHeaderView_Previews: PreviewProvider { private static let inactiveSessionViewData = UserOtherSessionsHeaderViewData(title: "Inactive sessions", - subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore. Learn more", + subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore.", iconName: Asset.Images.userOtherSessionsInactive.name) From ca3a401b1853e315b291078e9fbc62a8ae362a0b Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 10:07:04 +0300 Subject: [PATCH 04/11] Added translation strings, cleanup --- Riot/Assets/en.lproj/Vector.strings | 5 +++++ Riot/Generated/Strings.swift | 12 ++++++++++++ .../Coordinator/UserSessionsFlowCoordinator.swift | 3 +-- .../Coordinator/UserOtherSessionsCoordinator.swift | 13 ------------- .../MockUserOtherSessionsScreenState.swift | 4 +++- .../UserOtherSessionsViewModel.swift | 7 +++---- .../UserOtherSessions/View/UserOtherSessions.swift | 1 - .../View/UserOtherSessionsHeaderView.swift | 5 +++-- .../UserSessionsOverviewCoordinator.swift | 2 +- .../View/UserSessionListItemViewDataFactory.swift | 5 ++--- 10 files changed, 30 insertions(+), 27 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 79cf31ba0e..98913788a6 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -2385,11 +2385,16 @@ To enable access, tap Settings> Location and select Always"; "user_session_verified_additional_info" = "Your current session is ready for secure messaging."; "user_session_unverified_additional_info" = "Verify your current session for enhanced secure messaging."; +"user_other_session_security_recommendation_title" = "Security recommendation"; + + // First item is client name and second item is session display name "user_session_name" = "%@: %@"; "user_session_item_details" = "%@ · Last activity %@"; +"user_inactive_session_item" = "Inactive for 90+ days"; +"user_inactive_session_item_with_date" = "Inactive for 90+ days (%@)"; "device_name_desktop" = "%@ Desktop"; "device_name_web" = "%@ Web"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 31d85976dc..4575e4982e 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -8467,6 +8467,18 @@ public class VectorL10n: NSObject { public static var userIdTitle: String { return VectorL10n.tr("Vector", "user_id_title") } + /// Inactive for 90+ days + public static var userInactiveSessionItem: String { + return VectorL10n.tr("Vector", "user_inactive_session_item") + } + /// Inactive for 90+ days (%@) + public static func userInactiveSessionItemWithDate(_ p1: String) -> String { + return VectorL10n.tr("Vector", "user_inactive_session_item_with_date", p1) + } + /// Security recommendation + public static var userOtherSessionSecurityRecommendationTitle: String { + return VectorL10n.tr("Vector", "user_other_session_security_recommendation_title") + } /// IP address public static var userSessionDetailsDeviceIpAddress: String { return VectorL10n.tr("Vector", "user_session_details_device_ip_address") diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 48b205dc99..6a3313d089 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -56,8 +56,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { case let .openSessionOverview(session: session): self.openSessionOverview(session: session) case let .openOtherSessions(sessions: sessions, filter: filter): - let title = filter == .all ? "Other sessions" : "Security recommendation" - self.openOtherSessions(sessions: sessions, filterBy: filter, title: title) + self.openOtherSessions(sessions: sessions, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle) } } return coordinator diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift index 1216989f68..1ee00f38cf 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift @@ -67,17 +67,4 @@ final class UserOtherSessionsCoordinator: Coordinator, Presentable { } // MARK: - Private - - /// Show an activity indicator whilst loading. - /// - Parameters: - /// - label: The label to show on the indicator. - /// - isInteractionBlocking: Whether the indicator should block any user interaction. - private func startLoading(label: String = VectorL10n.loading, isInteractionBlocking: Bool = true) { - loadingIndicator = indicatorPresenter.present(.loading(label: label, isInteractionBlocking: isInteractionBlocking)) - } - - /// Hide the currently displayed activity indicator. - private func stopLoading() { - loadingIndicator = nil - } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift index 878d0e5d67..098aac1937 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift @@ -40,7 +40,9 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let viewModel = UserOtherSessionsViewModel(sessions: inactiveSessions(), filter: .inactive, title: "Security recommendation") + let viewModel = UserOtherSessionsViewModel(sessions: inactiveSessions(), + filter: .inactive, + title: VectorL10n.userOtherSessionSecurityRecommendationTitle) // can simulate service and viewModel actions here if needs be. diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 339bc01ed1..296bae71b6 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -36,7 +36,7 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi switch viewAction { case let .userOtherSessionSelected(sessionId: sessionId): guard let session = sessions.first(where: {$0.id == sessionId}) else { - assertionFailure("Shouldn't happen, session should be present in the array.") + assertionFailure("Session should exist in the array.") return } completion?(.showUserSessionOverview(session: session)) @@ -70,15 +70,14 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi subtitle: "", iconName: nil) case .inactive: - return UserOtherSessionsHeaderViewData(title: "Inactive sessions", - subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore.", + return UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle, + subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo, iconName: Asset.Images.userOtherSessionsInactive.name) case .unverified: // TODO: return UserOtherSessionsHeaderViewData(title: nil, subtitle: "", iconName: nil) - } } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift index 07207e1d99..320a045987 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessions.swift @@ -30,7 +30,6 @@ struct UserOtherSessions: View { createSessionItemsSection(header: header, items: items) } } - } .background(theme.colors.system.ignoresSafeArea()) .frame(maxHeight: .infinity) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift index e436142255..d6a2b344b9 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/View/UserOtherSessionsHeaderView.swift @@ -61,11 +61,12 @@ struct UserOtherSessionsHeaderView: View { } } +// MARK: - Previews struct UserOtherSessionsHeaderView_Previews: PreviewProvider { - private static let inactiveSessionViewData = UserOtherSessionsHeaderViewData(title: "Inactive sessions", - subtitle: "Consider signing out from old sessions (90 days or older) you don’t use anymore.", + private static let inactiveSessionViewData = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle, + subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo, iconName: Asset.Images.userOtherSessionsInactive.name) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift index 2aad0fae17..2d77234044 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift @@ -88,7 +88,7 @@ final class UserSessionsOverviewCoordinator: Coordinator, Presentable { } private func startVerifyCurrentSession() { - // TODO:openSessionOverview + // TODO: } private func showCurrentSessionOverview(session: UserSessionInfo) { diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift index 2e6bb38a49..c21e46b245 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewDataFactory.swift @@ -48,9 +48,9 @@ struct UserSessionListItemViewDataFactory { private func inactiveSessionDetails(lastActivityDate: TimeInterval?) -> String { if let lastActivityDate = lastActivityDate { let lastActivityDateString = Self.inactiveSessionDateFormatter.lastActivityDateString(from: lastActivityDate) - return "Inactive for 90+ days (\(lastActivityDateString))" + return VectorL10n.userInactiveSessionItemWithDate(lastActivityDateString) } - return "Inactive for 90+ days" + return VectorL10n.userInactiveSessionItem } private func activeSessionDetails(isVerified: Bool, lastActivityDate: TimeInterval?) -> String { @@ -76,5 +76,4 @@ struct UserSessionListItemViewDataFactory { private func getSessionDetailsIcon(isActive: Bool) -> String? { isActive ? nil : Asset.Images.userSessionListItemInactiveSession.name } - } From 34aa035ead26a84a4c762f7c4b43046593e5c330 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 15:34:31 +0300 Subject: [PATCH 05/11] Renamed session to sessionInfo --- ...tiveUserSessionLastActivityFormatter.swift | 1 - .../UserSessionsFlowCoordinator.swift | 15 ++++---- .../UserOtherSessionsModels.swift | 6 ---- .../UserOtherSessionsViewModel.swift | 6 ++++ .../UserSessionDetailsCoordinator.swift | 2 +- .../MockUserSessionDetailsScreenState.swift | 10 +++--- .../UserSessionDetailsViewModelTests.swift | 6 ++-- .../UserSessionDetailsViewModel.swift | 36 +++++++++---------- .../UserSessionOverviewViewModelTests.swift | 2 +- 9 files changed, 42 insertions(+), 42 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift b/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift index 33eddd114b..f44ef9204e 100644 --- a/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift +++ b/RiotSwiftUI/Modules/UserSessions/Common/InactiveUserSessionLastActivityFormatter.swift @@ -27,7 +27,6 @@ class InactiveUserSessionLastActivityFormatter { static func lastActivityDateString(from lastActivityTimestamp: TimeInterval) -> String { let date = Date(timeIntervalSince1970: lastActivityTimestamp) - return InactiveUserSessionLastActivityFormatter.dateFormatter.string(from: date) } } diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index ecfecd9a20..39dc3861eb 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -54,7 +54,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { guard let self = self else { return } switch result { case let .openSessionOverview(sessionInfo: sessionInfo): - self.openSessionOverview(sessionsInfo: sessionInfo) + self.openSessionOverview(sessionInfo: sessionInfo) case let .openOtherSessions(sessionsInfo: sessionsInfo, filter: filter): self.openOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle) } @@ -72,20 +72,21 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { return UserSessionDetailsCoordinator(parameters: parameters) } - private func openSessionOverview(sessionsInfo: UserSessionInfo) { - let coordinator = createUserSessionOverviewCoordinator(sessionsInfo: sessionsInfo) + private func openSessionOverview(sessionInfo: UserSessionInfo) { + let coordinator = createUserSessionOverviewCoordinator(sessionInfo: sessionInfo) coordinator.completion = { [weak self] result in guard let self = self else { return } switch result { - case let .openSessionDetails(sessionInfo: session): - self.openSessionDetails(sessionInfo: session) + case let .openSessionDetails(sessionInfo: sessionInfo): + self.openSessionDetails(sessionInfo: sessionInfo) } } pushScreen(with: coordinator) } - private func createUserSessionOverviewCoordinator(sessionsInfo: UserSessionInfo) -> UserSessionOverviewCoordinator { - let parameters = UserSessionOverviewCoordinatorParameters(session: parameters.session, sessionInfo: sessionsInfo) + private func createUserSessionOverviewCoordinator(sessionInfo: UserSessionInfo) -> UserSessionOverviewCoordinator { + let parameters = UserSessionOverviewCoordinatorParameters(session: parameters.session, + sessionInfo: sessionInfo) return UserSessionOverviewCoordinator(parameters: parameters) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index e2586c83c5..af962896ce 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -44,9 +44,3 @@ enum UserOtherSessionsSection: Hashable, Identifiable { enum UserOtherSessionsViewAction { case userOtherSessionSelected(sessionId: String) } - -enum OtherUserSessionsFilter { - case all - case inactive - case unverified -} diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 5a90b82308..98901aac91 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -18,6 +18,12 @@ import SwiftUI typealias UserOtherSessionsViewModelType = StateStoreViewModel +enum OtherUserSessionsFilter { + case all + case inactive + case unverified +} + class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol { var completion: ((UserOtherSessionsViewModelResult) -> Void)? private let sessionsInfo: [UserSessionInfo] diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift index f50c62811e..20aaee1538 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Coordinator/UserSessionDetailsCoordinator.swift @@ -40,7 +40,7 @@ final class UserSessionDetailsCoordinator: Coordinator, Presentable { init(parameters: UserSessionDetailsCoordinatorParameters) { self.parameters = parameters - let viewModel = UserSessionDetailsViewModel(session: parameters.sessionInfo) + let viewModel = UserSessionDetailsViewModel(sessionInfo: parameters.sessionInfo) let view = UserSessionDetails(viewModel: viewModel.context) userSessionDetailsViewModel = viewModel userSessionDetailsHostingController = VectorHostingController(rootView: view) diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift index 37233e141b..66fb5ad051 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/MockUserSessionDetailsScreenState.swift @@ -38,10 +38,10 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable { /// Generate the view struct for the screen state. var screenView: ([Any], AnyView) { - let session: UserSessionInfo + let sessionInfo: UserSessionInfo switch self { case .allSections: - session = UserSessionInfo(id: "alice", + sessionInfo = UserSessionInfo(id: "alice", name: "iOS", deviceType: .mobile, isVerified: false, @@ -57,7 +57,7 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable { isActive: true, isCurrent: true) case .sessionSectionOnly: - session = UserSessionInfo(id: "3", + sessionInfo = UserSessionInfo(id: "3", name: "Android", deviceType: .mobile, isVerified: false, @@ -73,12 +73,12 @@ enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable { isActive: true, isCurrent: false) } - let viewModel = UserSessionDetailsViewModel(session: session) + let viewModel = UserSessionDetailsViewModel(sessionInfo: sessionInfo) // can simulate service and viewModel actions here if needs be. return ( - [session], + [sessionInfo], AnyView(UserSessionDetails(viewModel: viewModel.context)) ) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift index 184725e1ec..66d3de41e6 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/Test/Unit/UserSessionDetailsViewModelTests.swift @@ -35,7 +35,7 @@ class UserSessionDetailsViewModelTests: XCTestCase { ] let expectedModel = UserSessionDetailsViewState(sections: sections) - let sut = UserSessionDetailsViewModel(session: userSessionInfo) + let sut = UserSessionDetailsViewModel(sessionInfo: userSessionInfo) XCTAssertEqual(sut.state, expectedModel) } @@ -57,7 +57,7 @@ class UserSessionDetailsViewModelTests: XCTestCase { ] let expectedModel = UserSessionDetailsViewState(sections: sections) - let sut = UserSessionDetailsViewModel(session: userSessionInfo) + let sut = UserSessionDetailsViewModel(sessionInfo: userSessionInfo) XCTAssertEqual(sut.state, expectedModel) } @@ -94,7 +94,7 @@ class UserSessionDetailsViewModelTests: XCTestCase { ] let expectedModel = UserSessionDetailsViewState(sections: sections) - let sut = UserSessionDetailsViewModel(session: userSessionInfo) + let sut = UserSessionDetailsViewModel(sessionInfo: userSessionInfo) XCTAssertEqual(sut.state, expectedModel) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift index 475f3bb5fe..41138c9e11 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionDetails/UserSessionDetailsViewModel.swift @@ -21,59 +21,59 @@ typealias UserSessionDetailsViewModelType = StateStoreViewModel Void)? - init(session: UserSessionInfo) { + init(sessionInfo: UserSessionInfo) { super.init(initialViewState: UserSessionDetailsViewState(sections: [])) - updateViewState(session: session) + updateViewState(sessionInfo: sessionInfo) } // MARK: - Public // MARK: - Private - private func updateViewState(session: UserSessionInfo) { + private func updateViewState(sessionInfo: UserSessionInfo) { var sections = [UserSessionDetailsSectionViewData]() - sections.append(sessionSection(session: session)) + sections.append(sessionSection(sessionInfo: sessionInfo)) - if let applicationSection = applicationSection(session: session) { + if let applicationSection = applicationSection(sessionInfo: sessionInfo) { sections.append(applicationSection) } - if let deviceSection = deviceSection(session: session) { + if let deviceSection = deviceSection(sessionInfo: sessionInfo) { sections.append(deviceSection) } state = UserSessionDetailsViewState(sections: sections) } - private func sessionSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData { + private func sessionSection(sessionInfo: UserSessionInfo) -> UserSessionDetailsSectionViewData { var sessionItems: [UserSessionDetailsSectionItemViewData] = [] - if let sessionName = session.name { + if let sessionName = sessionInfo.name { sessionItems.append(.init(title: VectorL10n.userSessionDetailsSessionName, value: sessionName)) } sessionItems.append(.init(title: VectorL10n.keyVerificationManuallyVerifyDeviceIdTitle, - value: session.id)) + value: sessionInfo.id)) return .init(header: VectorL10n.userSessionDetailsSessionSectionHeader.uppercased(), footer: VectorL10n.userSessionDetailsSessionSectionFooter, items: sessionItems) } - private func applicationSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? { + private func applicationSection(sessionInfo: UserSessionInfo) -> UserSessionDetailsSectionViewData? { var sessionItems: [UserSessionDetailsSectionItemViewData] = [] - if let name = session.applicationName { + if let name = sessionInfo.applicationName { sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationName, value: name)) } - if let version = session.applicationVersion { + if let version = sessionInfo.applicationVersion { sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationVersion, value: version)) } - if let url = session.applicationURL { + if let url = sessionInfo.applicationURL { sessionItems.append(.init(title: VectorL10n.userSessionDetailsApplicationUrl, value: url)) } @@ -86,22 +86,22 @@ class UserSessionDetailsViewModel: UserSessionDetailsViewModelType, UserSessionD items: sessionItems) } - private func deviceSection(session: UserSessionInfo) -> UserSessionDetailsSectionViewData? { + private func deviceSection(sessionInfo: UserSessionInfo) -> UserSessionDetailsSectionViewData? { var deviceSectionItems = [UserSessionDetailsSectionItemViewData]() - if let model = session.deviceModel { + if let model = sessionInfo.deviceModel { deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceModel, value: model)) } - if let deviceOS = session.deviceOS { + if let deviceOS = sessionInfo.deviceOS { deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceOs, value: deviceOS)) } - if let lastSeenIP = session.lastSeenIP { + if let lastSeenIP = sessionInfo.lastSeenIP { deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpAddress, value: lastSeenIP)) } - if let lastSeenIPLocation = session.lastSeenIPLocation { + if let lastSeenIPLocation = sessionInfo.lastSeenIPLocation { deviceSectionItems.append(.init(title: VectorL10n.userSessionDetailsDeviceIpLocation, value: lastSeenIPLocation)) } diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift index 846584fee2..0c5582d74e 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift @@ -85,7 +85,7 @@ class UserSessionOverviewViewModelTests: XCTestCase { sut.process(viewAction: .togglePushNotifications) XCTAssertEqual(sut.state.isPusherEnabled, true) } - + private func createUserSessionInfo() -> UserSessionInfo { UserSessionInfo(id: "session", name: "iOS", From 1da7dacfd912e497b6e5ed9e19bcd1a892a78727 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 15:34:51 +0300 Subject: [PATCH 06/11] Changelog --- changelog.d/6786.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6786.wip diff --git a/changelog.d/6786.wip b/changelog.d/6786.wip new file mode 100644 index 0000000000..0f1def9f76 --- /dev/null +++ b/changelog.d/6786.wip @@ -0,0 +1 @@ +Device manager: Inactive sessions screen. From aae5ccd5bc5bd5e4010a7eed4d7a44f02a4463d6 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 15:40:17 +0300 Subject: [PATCH 07/11] Fixed formating --- Config/BuildSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index 6e3b1007de..39de0496a6 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -419,5 +419,5 @@ final class BuildSettings: NSObject { static let syncLocalContacts: Bool = false // MARK: - New App Layout - static let newAppLayoutEnabled = true + static let newAppLayoutEnabled = true } From 5ec1bb32c30ecfebb364856fb6646538ba35fe22 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 15:44:09 +0300 Subject: [PATCH 08/11] Formating fixes --- .../Coordinator/UserSessionsFlowCoordinator.swift | 8 ++++++-- .../Coordinator/UserOtherSessionsCoordinator.swift | 2 +- .../UserOtherSessions/UserOtherSessionsViewModel.swift | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift index 39dc3861eb..5ad3cbbddd 100644 --- a/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift @@ -56,7 +56,9 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { case let .openSessionOverview(sessionInfo: sessionInfo): self.openSessionOverview(sessionInfo: sessionInfo) case let .openOtherSessions(sessionsInfo: sessionsInfo, filter: filter): - self.openOtherSessions(sessionsInfo: sessionsInfo, filterBy: filter, title: VectorL10n.userOtherSessionSecurityRecommendationTitle) + self.openOtherSessions(sessionsInfo: sessionsInfo, + filterBy: filter, + title: VectorL10n.userOtherSessionSecurityRecommendationTitle) } } return coordinator @@ -104,7 +106,9 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable { pushScreen(with: coordinator) } - private func createOtherSessionsCoordinator(sessionsInfo: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) -> UserOtherSessionsCoordinator { + private func createOtherSessionsCoordinator(sessionsInfo: [UserSessionInfo], + filterBy filter: OtherUserSessionsFilter, + title: String) -> UserOtherSessionsCoordinator { let parameters = UserOtherSessionsCoordinatorParameters(sessionsInfo: sessionsInfo, filter: filter, title: title) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift index a51e2a0f18..fd7fa89325 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Coordinator/UserOtherSessionsCoordinator.swift @@ -24,10 +24,10 @@ struct UserOtherSessionsCoordinatorParameters { } final class UserOtherSessionsCoordinator: Coordinator, Presentable { + private let parameters: UserOtherSessionsCoordinatorParameters private let userOtherSessionsHostingController: UIViewController private var userOtherSessionsViewModel: UserOtherSessionsViewModelProtocol - private var indicatorPresenter: UserIndicatorTypePresenterProtocol private var loadingIndicator: UserIndicator? diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift index 98901aac91..a3c3997912 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsViewModel.swift @@ -25,6 +25,7 @@ enum OtherUserSessionsFilter { } class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessionsViewModelProtocol { + var completion: ((UserOtherSessionsViewModelResult) -> Void)? private let sessionsInfo: [UserSessionInfo] From 347724f48e1f3a21727e310bb841e034e111d18b Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Tue, 4 Oct 2022 16:58:08 +0300 Subject: [PATCH 09/11] Added tests --- .../Test/UI/UserOtherSessionsUITests.swift | 12 +++++ .../UserOtherSessionsViewModelTests.swift | 52 ++++++++++++++++++- .../UserOtherSessionsModels.swift | 4 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift index 746acd8146..e5ae0f0c1c 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/UI/UserOtherSessionsUITests.swift @@ -19,4 +19,16 @@ import XCTest class UserOtherSessionsUITests: MockScreenTestCase { + func test_whenOtherSessionsWithInactiveSessionFilterPresented_correctHeaderDisplayed() { + app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.inactiveSessions.title) + + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle].exists) + XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo].exists) + } + + func test_whenOtherSessionsWithInactiveSessionFilterPresented_correctItemsDisplayed() { + app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.inactiveSessions.title) + + XCTAssertTrue(app.buttons["RiotSwiftUI Mobile: iOS, Inactive for 90+ days"].exists) + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index dc6ed7f106..8c19fc6be2 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -19,5 +19,55 @@ import XCTest @testable import RiotSwiftUI class UserOtherSessionsViewModelTests: XCTestCase { - + + var sut: UserOtherSessionsViewModel! + + func test_whenUserOtherSessionSelectedProcessed_completionWithShowUserSessionOverviewCalled() { + let expectedUserSessionInfo = createUserSessionInfo(sessionId: "session 2") + sut = UserOtherSessionsViewModel(sessionsInfo: [createUserSessionInfo(sessionId: "session 1"), + expectedUserSessionInfo], + filter: .inactive, + title: "Title") + + var modelResult: UserOtherSessionsViewModelResult? + sut.completion = { result in + modelResult = result + } + sut.process(viewAction: .userOtherSessionSelected(sessionId: expectedUserSessionInfo.id)) + XCTAssertEqual(modelResult, .showUserSessionOverview(sessionInfo: expectedUserSessionInfo)) + } + + func test_whenModelCreated_withInactiveFilter_viewStateIsCorrect() { + let sessionsInfo = [createUserSessionInfo(sessionId: "session 1"), createUserSessionInfo(sessionId: "session 2")] + sut = UserOtherSessionsViewModel(sessionsInfo: sessionsInfo, + filter: .inactive, + title: "Title") + + let expectedHeader = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle, + subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo, + iconName: Asset.Images.userOtherSessionsInactive.name) + let expectedItems = sessionsInfo.filter { !$0.isActive }.asViewData() + let expectedState = UserOtherSessionsViewState(title: "Title", + sections: [.sessionItems(header: expectedHeader, items: expectedItems)]) + XCTAssertEqual(sut.state, expectedState) + } + + + private func createUserSessionInfo(sessionId: String) -> UserSessionInfo { + UserSessionInfo(id: sessionId, + name: "iOS", + deviceType: .mobile, + isVerified: false, + lastSeenIP: "10.0.0.10", + lastSeenTimestamp: Date().timeIntervalSince1970 - 100, + applicationName: nil, + applicationVersion: nil, + applicationURL: nil, + deviceModel: "iPhone XS", + deviceOS: "iOS 15.5", + lastSeenIPLocation: nil, + deviceName: "Mobile", + isActive: true, + isCurrent: true) + } } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift index af962896ce..3cda39e336 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/UserOtherSessionsModels.swift @@ -23,13 +23,13 @@ enum UserOtherSessionsCoordinatorResult { // MARK: View model -enum UserOtherSessionsViewModelResult { +enum UserOtherSessionsViewModelResult: Equatable { case showUserSessionOverview(sessionInfo: UserSessionInfo) } // MARK: View -struct UserOtherSessionsViewState: BindableState { +struct UserOtherSessionsViewState: BindableState, Equatable { let title: String var sections: [UserOtherSessionsSection] } From 9dad6252e535384415e0aaa477dfea2b33e905dc Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 5 Oct 2022 09:18:33 +0300 Subject: [PATCH 10/11] Cleanup for UserSessionOverviewViewModelTestsc, UserOtherSessionsViewModelTests --- .../Test/Unit/UserOtherSessionsViewModelTests.swift | 5 ++--- .../Test/Unit/UserSessionOverviewViewModelTests.swift | 11 +++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index 8c19fc6be2..32fccd5b29 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -20,11 +20,10 @@ import XCTest class UserOtherSessionsViewModelTests: XCTestCase { - var sut: UserOtherSessionsViewModel! func test_whenUserOtherSessionSelectedProcessed_completionWithShowUserSessionOverviewCalled() { let expectedUserSessionInfo = createUserSessionInfo(sessionId: "session 2") - sut = UserOtherSessionsViewModel(sessionsInfo: [createUserSessionInfo(sessionId: "session 1"), + let sut = UserOtherSessionsViewModel(sessionsInfo: [createUserSessionInfo(sessionId: "session 1"), expectedUserSessionInfo], filter: .inactive, title: "Title") @@ -39,7 +38,7 @@ class UserOtherSessionsViewModelTests: XCTestCase { func test_whenModelCreated_withInactiveFilter_viewStateIsCorrect() { let sessionsInfo = [createUserSessionInfo(sessionId: "session 1"), createUserSessionInfo(sessionId: "session 2")] - sut = UserOtherSessionsViewModel(sessionsInfo: sessionsInfo, + let sut = UserOtherSessionsViewModel(sessionsInfo: sessionsInfo, filter: .inactive, title: "Title") diff --git a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift index 0c5582d74e..debfffbb83 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift @@ -20,10 +20,9 @@ import XCTest @testable import RiotSwiftUI class UserSessionOverviewViewModelTests: XCTestCase { - var sut: UserSessionOverviewViewModel! func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() { - sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService()) + let sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService()) XCTAssertEqual(sut.state.isPusherEnabled, nil) var modelResult: UserSessionOverviewViewModelResult? @@ -36,7 +35,7 @@ class UserSessionOverviewViewModelTests: XCTestCase { func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() { let sessionInfo = createUserSessionInfo() - sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: MockUserSessionOverviewService()) + let sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: MockUserSessionOverviewService()) XCTAssertEqual(sut.state.isPusherEnabled, nil) var modelResult: UserSessionOverviewViewModelResult? @@ -50,7 +49,7 @@ class UserSessionOverviewViewModelTests: XCTestCase { func test_whenViewSessionDetailsProcessed_toggleAvailablePusher() { let sessionInfo = createUserSessionInfo() let service = MockUserSessionOverviewService(pusherEnabled: true) - sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) + let sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable) XCTAssertEqual(sut.state.isPusherEnabled, true) @@ -63,7 +62,7 @@ class UserSessionOverviewViewModelTests: XCTestCase { func test_whenViewSessionDetailsProcessed_toggleNoPusher() { let sessionInfo = createUserSessionInfo() let service = MockUserSessionOverviewService(pusherEnabled: nil) - sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) + let sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable) XCTAssertEqual(sut.state.isPusherEnabled, nil) @@ -76,7 +75,7 @@ class UserSessionOverviewViewModelTests: XCTestCase { func test_whenViewSessionDetailsProcessed_remotelyTogglingPushersNotAvailable() { let sessionInfo = createUserSessionInfo() let service = MockUserSessionOverviewService(pusherEnabled: true, remotelyTogglingPushersAvailable: false) - sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) + let sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service) XCTAssertFalse(sut.state.remotelyTogglingPushersAvailable) XCTAssertEqual(sut.state.isPusherEnabled, true) From 87cc0ddc81a850674b7c830baf1243aa149ac0b1 Mon Sep 17 00:00:00 2001 From: Aleksandrs Proskurins Date: Wed, 5 Oct 2022 12:18:23 +0300 Subject: [PATCH 11/11] Merge fixes --- .../MockUserOtherSessionsScreenState.swift | 12 ++++++++---- .../Test/Unit/UserOtherSessionsViewModelTests.swift | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift index 2b7fdaa229..788311fa99 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/MockUserOtherSessionsScreenState.swift @@ -65,7 +65,8 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: nil, lastSeenIPLocation: nil, - deviceName: "", + clientName: nil, + clientVersion: nil, isActive: false, isCurrent: true), UserSessionInfo(id: "1", @@ -80,7 +81,8 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: nil, lastSeenIPLocation: nil, - deviceName: "", + clientName: nil, + clientVersion: nil, isActive: false, isCurrent: false), UserSessionInfo(id: "2", @@ -95,7 +97,8 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: nil, lastSeenIPLocation: nil, - deviceName: "", + clientName: nil, + clientVersion: nil, isActive: false, isCurrent: false), UserSessionInfo(id: "3", @@ -110,7 +113,8 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable { deviceModel: nil, deviceOS: nil, lastSeenIPLocation: nil, - deviceName: "", + clientName: nil, + clientVersion: nil, isActive: false, isCurrent: false)] } diff --git a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift index 32fccd5b29..43bf5c3580 100644 --- a/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift +++ b/RiotSwiftUI/Modules/UserSessions/UserOtherSessions/Test/Unit/UserOtherSessionsViewModelTests.swift @@ -65,7 +65,8 @@ class UserOtherSessionsViewModelTests: XCTestCase { deviceModel: "iPhone XS", deviceOS: "iOS 15.5", lastSeenIPLocation: nil, - deviceName: "Mobile", + clientName: nil, + clientVersion: nil, isActive: true, isCurrent: true) }