'View all' button in other sessions list

This commit is contained in:
Aleksandrs Proskurins
2022-10-07 09:45:45 +03:00
parent f9ce969dbf
commit dfe5780dc3
11 changed files with 251 additions and 21 deletions
@@ -72,9 +72,7 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
case let .openSessionOverview(sessionInfo: sessionInfo):
self.openSessionOverview(sessionInfo: sessionInfo)
case let .openOtherSessions(sessionInfos: sessionInfos, filter: filter):
self.openOtherSessions(sessionInfos: sessionInfos,
filterBy: filter,
title: VectorL10n.userOtherSessionSecurityRecommendationTitle)
self.openOtherSessions(sessionInfos: sessionInfos, filterBy: filter)
}
}
return coordinator
@@ -112,7 +110,9 @@ final class UserSessionsFlowCoordinator: Coordinator, Presentable {
return UserSessionOverviewCoordinator(parameters: parameters)
}
private func openOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter, title: String) {
private func openOtherSessions(sessionInfos: [UserSessionInfo], filterBy filter: OtherUserSessionsFilter) {
let title = filter == .all ? VectorL10n.userSessionsOverviewOtherSessionsSectionTitle :
VectorL10n.userOtherSessionSecurityRecommendationTitle
let coordinator = createOtherSessionsCoordinator(sessionInfos: sessionInfos,
filterBy: filter,
title: title)
@@ -24,6 +24,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
// with specific, minimal associated data that will allow you
// mock that screen.
case all
case inactiveSessions
case unverifiedSessions
@@ -35,7 +36,7 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
/// A list of screen state definitions
static var allCases: [MockUserOtherSessionsScreenState] {
// Each of the presence statuses
[.inactiveSessions, .unverifiedSessions]
[.all, .inactiveSessions, .unverifiedSessions]
}
/// Generate the view struct for the screen state.
@@ -43,6 +44,10 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
let viewModel: UserOtherSessionsViewModel
switch self {
case .all:
viewModel = UserOtherSessionsViewModel(sessionInfos: allSessions(),
filter: .all,
title: VectorL10n.userSessionsOverviewOtherSessionsSectionTitle)
case .inactiveSessions:
viewModel = UserOtherSessionsViewModel(sessionInfos: inactiveSessions(),
filter: .inactive,
@@ -163,4 +168,103 @@ enum MockUserOtherSessionsScreenState: MockScreenState, CaseIterable {
isCurrent: false)
]
}
private func allSessions() -> [UserSessionInfo] {
[UserSessionInfo(id: "0",
name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 500000,
applicationName: nil,
applicationVersion: nil,
applicationURL: nil,
deviceModel: nil,
deviceOS: nil,
lastSeenIPLocation: nil,
clientName: nil,
clientVersion: nil,
isActive: false,
isCurrent: false),
UserSessionInfo(id: "1",
name: "macOS",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "1.0.0.1",
lastSeenTimestamp: Date().timeIntervalSince1970 - 8000000,
applicationName: nil,
applicationVersion: nil,
applicationURL: nil,
deviceModel: nil,
deviceOS: nil,
lastSeenIPLocation: nil,
clientName: nil,
clientVersion: nil,
isActive: false,
isCurrent: false),
UserSessionInfo(id: "2",
name: "Firefox on Windows",
deviceType: .web,
isVerified: true,
lastSeenIP: "2.0.0.2",
lastSeenTimestamp: Date().timeIntervalSince1970 - 9000000,
applicationName: nil,
applicationVersion: nil,
applicationURL: nil,
deviceModel: nil,
deviceOS: nil,
lastSeenIPLocation: nil,
clientName: nil,
clientVersion: nil,
isActive: false,
isCurrent: false),
UserSessionInfo(id: "3",
name: "Android",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "3.0.0.3",
lastSeenTimestamp: Date().timeIntervalSince1970 - 10000000,
applicationName: nil,
applicationVersion: nil,
applicationURL: nil,
deviceModel: nil,
deviceOS: nil,
lastSeenIPLocation: nil,
clientName: nil,
clientVersion: nil,
isActive: false,
isCurrent: false),
UserSessionInfo(id: "4",
name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 11000000,
applicationName: nil,
applicationVersion: nil,
applicationURL: nil,
deviceModel: nil,
deviceOS: nil,
lastSeenIPLocation: nil,
clientName: nil,
clientVersion: nil,
isActive: false,
isCurrent: false),
UserSessionInfo(id: "5",
name: "macOS",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "1.0.0.1",
lastSeenTimestamp: Date().timeIntervalSince1970 - 20000000,
applicationName: nil,
applicationVersion: nil,
applicationURL: nil,
deviceModel: nil,
deviceOS: nil,
lastSeenIPLocation: nil,
clientName: nil,
clientVersion: nil,
isActive: false,
isCurrent: false)]
}
}
@@ -44,4 +44,10 @@ class UserOtherSessionsUITests: MockScreenTestCase {
XCTAssertTrue(app.buttons["RiotSwiftUI Mobile: iOS, Unverified · Your current session"].exists)
}
func test_whenOtherSessionsWithAllSessionFilterPresented_correctHeaderDisplayed() {
app.goToScreenWithIdentifier(MockUserOtherSessionsScreenState.all.title)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionsOverviewOtherSessionsSectionInfo].exists)
}
}
@@ -51,6 +51,21 @@ class UserOtherSessionsViewModelTests: XCTestCase {
XCTAssertEqual(sut.state, expectedState)
}
func test_whenModelCreated_withAllFilter_viewStateIsCorrect() {
let sessionInfos = [createUserSessionInfo(sessionId: "session 1"), createUserSessionInfo(sessionId: "session 2")]
let sut = UserOtherSessionsViewModel(sessionInfos: sessionInfos,
filter: .all,
title: "Title")
let expectedHeader = UserOtherSessionsHeaderViewData(title: nil,
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
iconName: nil)
let expectedItems = sessionInfos.filter { !$0.isCurrent }.asViewData()
let expectedState = UserOtherSessionsViewState(title: "Title",
sections: [.sessionItems(header: expectedHeader, items: expectedItems)])
XCTAssertEqual(sut.state, expectedState)
}
private func createUserSessionInfo(sessionId: String) -> UserSessionInfo {
UserSessionInfo(id: sessionId,
@@ -80,9 +80,8 @@ class UserOtherSessionsViewModel: UserOtherSessionsViewModelType, UserOtherSessi
private func createHeaderData(filter: OtherUserSessionsFilter) -> UserOtherSessionsHeaderViewData {
switch filter {
case .all:
// TODO:
return UserOtherSessionsHeaderViewData(title: nil,
subtitle: "",
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
iconName: nil)
case .inactive:
return UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
@@ -40,6 +40,7 @@ struct UserOtherSessionsHeaderView: View {
.background(theme.colors.background)
.clipShape(backgroundShape)
.shapedBorder(color: theme.colors.quinaryContent, borderWidth: 1.0, shape: backgroundShape)
.padding(.trailing, 16)
}
VStack(alignment: .leading, spacing: 0, content: {
if let title = viewData.title {
@@ -53,7 +54,6 @@ struct UserOtherSessionsHeaderView: View {
.foregroundColor(theme.colors.secondaryContent)
.padding(.bottom, 20.0)
})
.padding(.leading, 16)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
@@ -64,19 +64,36 @@ struct UserOtherSessionsHeaderView: View {
struct UserOtherSessionsHeaderView_Previews: PreviewProvider {
private static let inactiveSessionViewData = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
iconName: Asset.Images.userOtherSessionsInactive.name)
private static let headerWithTitleSubtitleIcon = UserOtherSessionsHeaderViewData(title: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveTitle,
subtitle: VectorL10n.userSessionsOverviewSecurityRecommendationsInactiveInfo,
iconName: Asset.Images.userOtherSessionsInactive.name)
private static let headerWithSubtitle = UserOtherSessionsHeaderViewData(title: nil,
subtitle: VectorL10n.userSessionsOverviewOtherSessionsSectionInfo,
iconName: nil)
static var previews: some View {
Group {
UserOtherSessionsHeaderView(viewData: self.inactiveSessionViewData)
.theme(.light)
.preferredColorScheme(.light)
UserOtherSessionsHeaderView(viewData: self.inactiveSessionViewData)
.theme(.dark)
.preferredColorScheme(.dark)
VStack {
Divider()
UserOtherSessionsHeaderView(viewData: self.headerWithTitleSubtitleIcon)
Divider()
UserOtherSessionsHeaderView(viewData: self.headerWithSubtitle)
Divider()
}
.theme(.light)
.preferredColorScheme(.light)
VStack {
Divider()
UserOtherSessionsHeaderView(viewData: self.headerWithTitleSubtitleIcon)
Divider()
UserOtherSessionsHeaderView(viewData: self.headerWithSubtitle)
Divider()
}
.theme(.dark)
.preferredColorScheme(.dark)
}
}
}
@@ -52,4 +52,16 @@ class UserSessionsOverviewUITests: MockScreenTestCase {
XCTAssertFalse(app.staticTexts["userSessionsOverviewSecurityRecommendationsSection"].exists)
XCTAssertFalse(app.staticTexts["userSessionsOverviewOtherSection"].exists)
}
func testWhenMoreThan5OtherSessionsThenViewAllButtonVisible() {
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.currentSessionUnverified.title)
app.swipeUp()
XCTAssertTrue(app.buttons["ViewAllButton"].exists)
}
func testWhenLessThan5OtherSessionsThenViewAllButtonHidden() {
app.goToScreenWithIdentifier(MockUserSessionsOverviewScreenState.onlyUnverifiedSessions.title)
app.swipeUp()
XCTAssertFalse(app.buttons["ViewAllButton"].exists)
}
}
@@ -50,8 +50,13 @@ class UserSessionsOverviewViewModelTests: XCTestCase {
viewModel.process(viewAction: .verifyCurrentSession)
XCTAssertEqual(result, .verifyCurrentSession)
result = nil
viewModel.process(viewAction: .viewAllInactiveSessions)
XCTAssertEqual(result, .showOtherSessions(sessionInfos: [], filter: .inactive))
result = nil
viewModel.process(viewAction: .viewAllOtherSessions)
XCTAssertEqual(result, .showOtherSessions(sessionInfos: [], filter: .all))
}
func testShowSessionDetails() {
@@ -20,7 +20,7 @@ typealias UserSessionsOverviewViewModelType = StateStoreViewModel<UserSessionsOv
class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSessionsOverviewViewModelProtocol {
private let userSessionsOverviewService: UserSessionsOverviewServiceProtocol
var completion: ((UserSessionsOverviewViewModelResult) -> Void)?
init(userSessionsOverviewService: UserSessionsOverviewServiceProtocol) {
@@ -62,8 +62,7 @@ class UserSessionsOverviewViewModel: UserSessionsOverviewViewModelType, UserSess
case .viewAllInactiveSessions:
showSessions(filteredBy: .inactive)
case .viewAllOtherSessions:
// TODO: showSessions(filteredBy: .all)
break
showSessions(filteredBy: .all)
case .tapUserSession(let sessionId):
guard let session = userSessionsOverviewService.sessionForIdentifier(sessionId) else {
assertionFailure("Missing session info")
@@ -0,0 +1,66 @@
//
// 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 UserSessionsListViewAllView: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
let count: Int
var onBackgroundTap: (() -> Void)?
var body: some View {
Button {
onBackgroundTap?()
} label: {
Button(action: { onBackgroundTap?() }) {
VStack(spacing: 0) {
HStack {
Text("View all (\(count))")
.font(theme.fonts.body)
.foregroundColor(theme.colors.accent)
.frame(maxWidth: .infinity, alignment: .leading)
Image(Asset.Images.chevron.name)
}
.padding(.vertical, 15)
.padding(.trailing, 20)
SeparatorLine()
}
.background(theme.colors.background)
.padding(.leading, 72)
}
}
.accessibilityIdentifier("ViewAllButton")
}
}
struct UserSessionsListViewAllView_Previews: PreviewProvider {
static var previews: some View {
Group {
UserSessionsListViewAllView(count: 8)
.previewLayout(PreviewLayout.sizeThatFits)
.theme(.light)
.preferredColorScheme(.light)
UserSessionsListViewAllView(count: 8)
.previewLayout(PreviewLayout.sizeThatFits)
.theme(.dark)
.preferredColorScheme(.dark)
}
}
}
@@ -21,6 +21,8 @@ struct UserSessionsOverview: View {
@ObservedObject var viewModel: UserSessionsOverviewViewModel.Context
private let maxOtherSessionsToDisplay = 5
var body: some View {
ScrollView {
if hasSecurityRecommendations {
@@ -132,11 +134,16 @@ struct UserSessionsOverview: View {
private var otherSessionsSection: some View {
SwiftUI.Section {
LazyVStack(spacing: 0) {
ForEach(viewModel.viewState.otherSessionsViewData) { viewData in
ForEach(viewModel.viewState.otherSessionsViewData.prefix(maxOtherSessionsToDisplay)) { viewData in
UserSessionListItem(viewData: viewData, onBackgroundTap: { sessionId in
viewModel.send(viewAction: .tapUserSession(sessionId))
})
}
if viewModel.viewState.otherSessionsViewData.count > maxOtherSessionsToDisplay {
UserSessionsListViewAllView(count: viewModel.viewState.otherSessionsViewData.count) {
viewModel.send(viewAction: .viewAllOtherSessions)
}
}
}
.background(theme.colors.background)
} header: {