mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-20 00:24:43 +02:00
Session overview screen
This commit is contained in:
+77
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// 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
|
||||
import CommonKit
|
||||
|
||||
struct UserSessionOverviewCoordinatorParameters {
|
||||
let userSessionInfo: UserSessionInfo
|
||||
let isCurrentSession: Bool
|
||||
}
|
||||
|
||||
final class UserSessionOverviewCoordinator: Coordinator, Presentable {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private let parameters: UserSessionOverviewCoordinatorParameters
|
||||
private let userSessionOverviewHostingController: UIViewController
|
||||
private var userSessionOverviewViewModel: UserSessionOverviewViewModelProtocol
|
||||
|
||||
private var indicatorPresenter: UserIndicatorTypePresenterProtocol
|
||||
private var loadingIndicator: UserIndicator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
// Must be used only internally
|
||||
var childCoordinators: [Coordinator] = []
|
||||
var completion: ((UserSessionOverviewCoordinatorResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(parameters: UserSessionOverviewCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
let viewModel = UserSessionOverviewViewModel(userSessionInfo: parameters.userSessionInfo, isCurrentSession: parameters.isCurrentSession)
|
||||
let view = UserSessionOverview(viewModel: viewModel.context)
|
||||
userSessionOverviewViewModel = viewModel
|
||||
userSessionOverviewHostingController = VectorHostingController(rootView: view)
|
||||
|
||||
indicatorPresenter = UserIndicatorTypePresenter(presentingViewController: userSessionOverviewHostingController)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
MXLog.debug("[UserSessionOverviewCoordinator] did start.")
|
||||
userSessionOverviewViewModel.completion = { [weak self] result in
|
||||
guard let self = self else { return }
|
||||
MXLog.debug("[UserSessionOverviewCoordinator] UserSessionOverviewViewModel did complete with result: \(result).")
|
||||
switch result {
|
||||
case .verifyCurrentSession:
|
||||
break // TODO
|
||||
case let .showCurrentSessionDetails(sessionInfo: sessionInfo):
|
||||
self.completion?(.openSessionDetails(session: sessionInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toPresentable() -> UIViewController {
|
||||
return self.userSessionOverviewHostingController
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// 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 MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
|
||||
// A case for each state you want to represent
|
||||
// with specific, minimal associated data that will allow you
|
||||
// mock that screen.
|
||||
case currentSession
|
||||
case otherSession
|
||||
|
||||
/// The associated screen
|
||||
var screenType: Any.Type {
|
||||
UserSessionOverview.self
|
||||
}
|
||||
|
||||
/// A list of screen state definitions
|
||||
static var allCases: [MockUserSessionOverviewScreenState] {
|
||||
// Each of the presence statuses
|
||||
return [.currentSession, .otherSession]
|
||||
}
|
||||
|
||||
/// Generate the view struct for the screen state.
|
||||
var screenView: ([Any], AnyView) {
|
||||
let viewModel: UserSessionOverviewViewModel
|
||||
switch self {
|
||||
case .currentSession:
|
||||
let currentSessionInfo = UserSessionInfo(sessionId: "session",
|
||||
sessionName: "iOS",
|
||||
deviceType: .mobile,
|
||||
isVerified: false,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100)
|
||||
viewModel = UserSessionOverviewViewModel(userSessionInfo: currentSessionInfo, isCurrentSession: true)
|
||||
case .otherSession:
|
||||
let currentSessionInfo = UserSessionInfo(sessionId: "session",
|
||||
sessionName: "Mac",
|
||||
deviceType: .desktop,
|
||||
isVerified: true,
|
||||
lastSeenIP: "10.0.0.10",
|
||||
lastSeenTimestamp: Date().timeIntervalSince1970 - 100)
|
||||
viewModel = UserSessionOverviewViewModel(userSessionInfo: currentSessionInfo, isCurrentSession: false)
|
||||
}
|
||||
|
||||
// can simulate service and viewModel actions here if needs be.
|
||||
|
||||
return (
|
||||
[viewModel],
|
||||
AnyView(UserSessionOverview(viewModel: viewModel.context))
|
||||
)
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// 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
|
||||
import RiotSwiftUI
|
||||
|
||||
class UserSessionOverviewUITests: MockScreenTestCase {
|
||||
func testUserSessionOverviewPresenceIdle() {
|
||||
let presence = UserSessionOverviewPresence.idle
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.presence(presence).title)
|
||||
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssertEqual(presenceText.label, presence.title)
|
||||
}
|
||||
|
||||
func testUserSessionOverviewPresenceOffline() {
|
||||
let presence = UserSessionOverviewPresence.offline
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.presence(presence).title)
|
||||
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssertEqual(presenceText.label, presence.title)
|
||||
}
|
||||
|
||||
func testUserSessionOverviewPresenceOnline() {
|
||||
let presence = UserSessionOverviewPresence.online
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.presence(presence).title)
|
||||
|
||||
let presenceText = app.staticTexts["presenceText"]
|
||||
XCTAssert(presenceText.exists)
|
||||
XCTAssertEqual(presenceText.label, presence.title)
|
||||
}
|
||||
|
||||
func testUserSessionOverviewLongName() {
|
||||
let name = "Somebody with a super long name we would like to test"
|
||||
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.longDisplayName(name).title)
|
||||
|
||||
let displayNameText = app.staticTexts["displayNameText"]
|
||||
XCTAssert(displayNameText.exists)
|
||||
XCTAssertEqual(displayNameText.label, name)
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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
|
||||
import Combine
|
||||
|
||||
@testable import RiotSwiftUI
|
||||
|
||||
class UserSessionOverviewViewModelTests: XCTestCase {
|
||||
private enum Constants {
|
||||
static let presenceInitialValue: UserSessionOverviewPresence = .offline
|
||||
static let displayName = "Alice"
|
||||
}
|
||||
var service: MockUserSessionOverviewService!
|
||||
var viewModel: UserSessionOverviewViewModelProtocol!
|
||||
var context: UserSessionOverviewViewModelType.Context!
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
override func setUpWithError() throws {
|
||||
service = MockUserSessionOverviewService(displayName: Constants.displayName, presence: Constants.presenceInitialValue)
|
||||
viewModel = UserSessionOverviewViewModel.makeUserSessionOverviewViewModel(userSessionOverviewService: service)
|
||||
context = viewModel.context
|
||||
}
|
||||
|
||||
func testInitialState() {
|
||||
XCTAssertEqual(context.viewState.displayName, Constants.displayName)
|
||||
XCTAssertEqual(context.viewState.presence, Constants.presenceInitialValue)
|
||||
}
|
||||
|
||||
func testFirstPresenceReceived() throws {
|
||||
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(1).first()
|
||||
XCTAssertEqual(try xcAwait(presencePublisher), [Constants.presenceInitialValue])
|
||||
}
|
||||
|
||||
func testPresenceUpdatesReceived() throws {
|
||||
let presencePublisher = context.$viewState.map(\.presence).removeDuplicates().collect(3).first()
|
||||
let awaitDeferred = xcAwaitDeferred(presencePublisher)
|
||||
let newPresenceValue1: UserSessionOverviewPresence = .online
|
||||
let newPresenceValue2: UserSessionOverviewPresence = .idle
|
||||
service.simulateUpdate(presence: newPresenceValue1)
|
||||
service.simulateUpdate(presence: newPresenceValue2)
|
||||
XCTAssertEqual(try awaitDeferred(), [Constants.presenceInitialValue, newPresenceValue1, newPresenceValue2])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum UserSessionOverviewCoordinatorResult {
|
||||
case openSessionDetails(session: UserSessionInfo)
|
||||
}
|
||||
|
||||
// MARK: View model
|
||||
|
||||
enum UserSessionOverviewViewModelResult {
|
||||
case showCurrentSessionDetails(sessionInfo: UserSessionInfo)
|
||||
case verifyCurrentSession
|
||||
}
|
||||
|
||||
// MARK: View
|
||||
|
||||
struct UserSessionOverviewViewState: BindableState {
|
||||
let cardViewData: UserSessionCardViewData
|
||||
let isCurrentSession: Bool
|
||||
}
|
||||
|
||||
enum UserSessionOverviewViewAction {
|
||||
case verifyCurrentSession
|
||||
case viewSessionDetails
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// 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 UserSessionOverviewViewModelType = StateStoreViewModel<UserSessionOverviewViewState,
|
||||
Never,
|
||||
UserSessionOverviewViewAction>
|
||||
|
||||
class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
private let userSessionInfo: UserSessionInfo
|
||||
// MARK: Public
|
||||
|
||||
var completion: ((UserSessionOverviewViewModelResult) -> Void)?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(userSessionInfo: UserSessionInfo, isCurrentSession: Bool) {
|
||||
self.userSessionInfo = userSessionInfo
|
||||
|
||||
let cardViewData = UserSessionCardViewData(userSessionInfo: userSessionInfo, isCurrentSessionDisplayMode: isCurrentSession)
|
||||
let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: isCurrentSession)
|
||||
super.init(initialViewState: state)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: UserSessionOverviewViewAction) {
|
||||
switch viewAction {
|
||||
case .verifyCurrentSession:
|
||||
completion?(.verifyCurrentSession)
|
||||
case .viewSessionDetails:
|
||||
completion?(.showCurrentSessionDetails(sessionInfo: userSessionInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -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 Foundation
|
||||
|
||||
protocol UserSessionOverviewViewModelProtocol {
|
||||
|
||||
var completion: ((UserSessionOverviewViewModelResult) -> Void)? { get set }
|
||||
var context: UserSessionOverviewViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// 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 UserSessionOverview: View {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
// MARK: Private
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
// MARK: Public
|
||||
|
||||
@ObservedObject var viewModel: UserSessionOverviewViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
UserSessionCardView(viewData: viewModel.viewState.cardViewData,
|
||||
onVerifyAction: { _ in
|
||||
viewModel.send(viewAction: .verifyCurrentSession)
|
||||
},
|
||||
onViewDetailsAction: { _ in
|
||||
viewModel.send(viewAction: .viewSessionDetails)
|
||||
})
|
||||
.padding(16)
|
||||
SwiftUI.Section {
|
||||
UserSessionOverviewDisclosureCell(title: "Session details", onBackgroundTap: {
|
||||
viewModel.send(viewAction: .viewSessionDetails)
|
||||
})
|
||||
}
|
||||
}
|
||||
.background(theme.colors.system.ignoresSafeArea())
|
||||
.frame(maxHeight: .infinity)
|
||||
.navigationTitle(viewModel.viewState.isCurrentSession ? "Current session" : "Session")
|
||||
}
|
||||
}
|
||||
|
||||
struct SeparatorLine: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
var body: some View {
|
||||
Rectangle()
|
||||
.fill(theme.colors.quinaryContent)
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
.frame(height: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct UserSessionOverview_Previews: PreviewProvider {
|
||||
static let stateRenderer = MockUserSessionOverviewScreenState.stateRenderer
|
||||
static var previews: some View {
|
||||
stateRenderer.screenGroup(addNavigation: true).theme(.light).preferredColorScheme(.light)
|
||||
stateRenderer.screenGroup(addNavigation: true).theme(.dark).preferredColorScheme(.dark)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// 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 UserSessionOverviewDisclosureCell: View {
|
||||
|
||||
@Environment(\.theme) private var theme: ThemeSwiftUI
|
||||
|
||||
let title: String
|
||||
var onBackgroundTap: (() -> (Void))? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SeparatorLine()
|
||||
HStack() {
|
||||
Text(title)
|
||||
.font(theme.fonts.body)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Image(Asset.Images.chevron.name)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
SeparatorLine()
|
||||
}
|
||||
.background(theme.colors.background)
|
||||
.onTapGesture {
|
||||
onBackgroundTap?()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user