User session flow coordinator and details view

This commit is contained in:
Aleksandrs Proskurins
2022-09-15 09:46:23 +03:00
parent 72451c8651
commit 066f179b60
10 changed files with 439 additions and 6 deletions
@@ -0,0 +1,57 @@
//
// 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
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
enum MockUserSessionDetailsScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case allSections
case sessionSectionOnly
/// The associated screen
var screenType: Any.Type {
UserSessionDetailsView.self
}
/// A list of screen state definitions
static var allCases: [MockUserSessionDetailsScreenState] {
// Each of the presence statuses
return [.allSections, sessionSectionOnly]
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let currentSessionInfo: UserSessionInfo
switch self {
case .allSections:
currentSessionInfo = UserSessionInfo(sessionId: "session", sessionName: "iOS", deviceType: .mobile, isVerified: false, lastSeenIP: "10.0.0.10", lastSeenTimestamp: Date().timeIntervalSince1970 - 100)
case .sessionSectionOnly:
currentSessionInfo = UserSessionInfo(sessionId: "session", sessionName: "iOS", deviceType: .mobile, isVerified: false, lastSeenIP: nil, lastSeenTimestamp: Date().timeIntervalSince1970 - 100)
}
let viewModel = UserSessionDetailsViewModel(userSessionInfo: currentSessionInfo)
// can simulate service and viewModel actions here if needs be.
return (
[currentSessionInfo],
AnyView(UserSessionDetailsView(viewModel: viewModel.context))
)
}
}
@@ -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 UserSessionDetailsItemView: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
let viewData: UserSessionDetailsSectionItemViewData
var body: some View {
HStack() {
Text(viewData.title)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.secondaryContent)
.frame(maxWidth: .infinity, alignment: .leading)
.frame(maxHeight: .infinity, alignment: .top)
Text(viewData.value)
.font(theme.fonts.subheadline)
.foregroundColor(theme.colors.primaryContent)
.multilineTextAlignment(.trailing)
}
.contextMenu {
Button {
UIPasteboard.general.string = viewData.value
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
}
.padding([.leading, .trailing], 20)
.padding([.top, .bottom], 12)
}
}
// MARK: - Previews
struct UserSessionDetailsItemView_Previews: PreviewProvider {
static var previews: some View {
Group {
List {
UserSessionDetailsItemView(viewData: UserSessionDetailsSectionItemViewData(title: "Session name", value: "123"))
.theme(.light)
.preferredColorScheme(.light)
.listRowInsets(EdgeInsets())
}
.listStyle(.grouped)
List {
UserSessionDetailsItemView(viewData: UserSessionDetailsSectionItemViewData(title: "Session name", value: "123"))
.theme(.dark)
.preferredColorScheme(.dark)
.listRowInsets(EdgeInsets())
}
.listStyle(.grouped)
}
}
}
@@ -0,0 +1,40 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum UserSessionDetailsViewModelResult {
}
enum UserSessionDetailsViewAction {
}
struct UserSessionDetailsViewState: BindableState {
let sections: [UserSessionDetailsSectionViewData]
}
struct UserSessionDetailsSectionViewData: Identifiable {
let id = UUID()
let header: String
let footer: String?
let items: [UserSessionDetailsSectionItemViewData]
}
struct UserSessionDetailsSectionItemViewData: Identifiable {
let id = UUID()
let title: String
let value: String
}
@@ -0,0 +1,71 @@
//
// 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 UserSessionDetailsView: View {
// MARK: - Properties
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ObservedObject var viewModel: UserSessionDetailsViewModel.Context
var body: some View {
List {
ForEach(viewModel.viewState.sections) { section in
SwiftUI.Section {
ForEach(section.items) { item in
UserSessionDetailsItemView(viewData: item)
.listRowInsets(EdgeInsets())
}
} header: {
Text(section.header)
.foregroundColor(theme.colors.secondaryContent)
.font(theme.fonts.footnote)
.padding([.leading, .trailing], 20)
.padding([.top, .bottom], 8)
} footer: {
if let footer = section.footer {
Text(footer)
.foregroundColor(theme.colors.secondaryContent)
.font(theme.fonts.footnote)
.padding([.leading, .trailing], 20)
.padding(.top, 8)
.padding(.bottom, 32)
}
}
.listRowInsets(EdgeInsets())
}
}
.listStyle(.grouped)
.navigationBarTitle("Session details", displayMode: .inline)
}
}
// MARK: - Previews
struct UserSessionDetails_Previews: PreviewProvider {
static let stateRenderer = MockUserSessionDetailsScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup(addNavigation: true).theme(.light).preferredColorScheme(.light)
stateRenderer.screenGroup(addNavigation: true).theme(.dark).preferredColorScheme(.dark)
}
}
@@ -0,0 +1,72 @@
//
// 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
typealias UserSessionDetailsViewModelType = StateStoreViewModel<UserSessionDetailsViewState,
Never,
UserSessionDetailsViewAction>
class UserSessionDetailsViewModel: UserSessionDetailsViewModelType {
// MARK: - Properties
// MARK: Private
// MARK: Public
var completion: ((UserSessionDetailsViewModelResult) -> Void)?
// MARK: - Setup
init(userSessionInfo: UserSessionInfo) {
var sections = [UserSessionDetailsSectionViewData]()
var sessionItems = [UserSessionDetailsSectionItemViewData]()
if let sessionName = userSessionInfo.sessionName {
sessionItems.append(UserSessionDetailsSectionItemViewData(title: "Session name",
value: sessionName))
}
sessionItems.append(UserSessionDetailsSectionItemViewData(title: "Session ID",
value: userSessionInfo.sessionId))
sections.append(UserSessionDetailsSectionViewData(header: "SESSION",
footer: "Copy any data by tapping on it and holding it down.",
items: sessionItems))
var deviceSectionItems = [UserSessionDetailsSectionItemViewData]()
if let lastSeenIP = userSessionInfo.lastSeenIP {
deviceSectionItems.append(UserSessionDetailsSectionItemViewData(title: "IP address",
value: lastSeenIP))
}
if deviceSectionItems.count > 0 {
sections.append(UserSessionDetailsSectionViewData(header: "DEVICE",
footer: nil,
items: deviceSectionItems))
}
let initialViewState = UserSessionDetailsViewState(sections: sections)
super.init(initialViewState: initialViewState)
}
// MARK: - Public
override func process(viewAction: UserSessionDetailsViewAction) {
}
// MARK: - Private
}