mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-18 21:52:18 +02:00
'View all' button in other sessions list
This commit is contained in:
@@ -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)
|
||||
|
||||
+105
-1
@@ -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)]
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -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,
|
||||
|
||||
+27
-10
@@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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() {
|
||||
|
||||
+2
-3
@@ -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")
|
||||
|
||||
+66
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user