Merge branch 'develop' into aleksandrs/6786_inactive_sessions_screen

# Conflicts:
#	Riot/Assets/en.lproj/Vector.strings
#	Riot/Generated/Strings.swift
#	RiotSwiftUI/Modules/UserSessions/Coordinator/UserSessionsFlowCoordinator.swift
#	RiotSwiftUI/Modules/UserSessions/UserSessionOverview/Test/Unit/UserSessionOverviewViewModelTests.swift
#	RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Coordinator/UserSessionsOverviewCoordinator.swift
#	RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/Service/Mock/MockUserSessionsOverviewService.swift
#	RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/UserSessionsOverviewModels.swift
#	RiotSwiftUI/Modules/UserSessions/UserSessionsOverview/View/UserSessionListItemViewData.swift
This commit is contained in:
Aleksandrs Proskurins
2022-10-04 15:14:59 +03:00
88 changed files with 2246 additions and 415 deletions
@@ -18,7 +18,8 @@ import CommonKit
import SwiftUI
struct UserSessionOverviewCoordinatorParameters {
let session: UserSessionInfo
let session: MXSession
let sessionInfo: UserSessionInfo
}
final class UserSessionOverviewCoordinator: Coordinator, Presentable {
@@ -40,7 +41,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
init(parameters: UserSessionOverviewCoordinatorParameters) {
self.parameters = parameters
viewModel = UserSessionOverviewViewModel(session: parameters.session)
let service = UserSessionOverviewService(session: parameters.session, sessionInfo: parameters.sessionInfo)
viewModel = UserSessionOverviewViewModel(sessionInfo: parameters.sessionInfo, service: service)
hostingController = VectorHostingController(rootView: UserSessionOverview(viewModel: viewModel.context))
@@ -57,8 +59,8 @@ final class UserSessionOverviewCoordinator: Coordinator, Presentable {
switch result {
case .verifyCurrentSession:
break // TODO:
case let .showSessionDetails(session: session):
self.completion?(.openSessionDetails(session: session))
case let .showSessionDetails(sessionInfo: sessionInfo):
self.completion?(.openSessionDetails(sessionInfo: sessionInfo))
}
}
}
@@ -25,7 +25,9 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
// mock that screen.
case currentSession
case otherSession
case sessionWithPushNotifications(enabled: Bool)
case remotelyTogglingPushersNotAvailable
/// The associated screen
var screenType: Any.Type {
UserSessionOverview.self
@@ -33,35 +35,89 @@ enum MockUserSessionOverviewScreenState: MockScreenState, CaseIterable {
/// A list of screen state definitions
static var allCases: [MockUserSessionOverviewScreenState] {
[.currentSession, .otherSession]
[.currentSession,
.otherSession,
.sessionWithPushNotifications(enabled: true),
.sessionWithPushNotifications(enabled: false),
.remotelyTogglingPushersNotAvailable]
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let viewModel: UserSessionOverviewViewModel
let session: UserSessionInfo
let service: UserSessionOverviewServiceProtocol
switch self {
case .currentSession:
let session = UserSessionInfo(id: "session",
name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
isActive: true,
isCurrent: true)
viewModel = UserSessionOverviewViewModel(session: session)
session = UserSessionInfo(id: "alice",
name: "iOS",
deviceType: .mobile,
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: nil,
applicationName: "Element iOS",
applicationVersion: "1.0.0",
applicationURL: nil,
deviceModel: nil,
deviceOS: "iOS 15.5",
lastSeenIPLocation: nil,
deviceName: "My iPhone",
isActive: true,
isCurrent: true)
service = MockUserSessionOverviewService()
case .otherSession:
let session = UserSessionInfo(id: "session",
name: "Mac",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
isActive: true,
isCurrent: false)
viewModel = UserSessionOverviewViewModel(session: session)
session = UserSessionInfo(id: "1",
name: "macOS",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "1.0.0.1",
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
applicationName: "Element MacOS",
applicationVersion: "1.0.0",
applicationURL: nil,
deviceModel: nil,
deviceOS: "macOS 12.5.1",
lastSeenIPLocation: nil,
deviceName: "My Mac",
isActive: false,
isCurrent: false)
service = MockUserSessionOverviewService()
case .sessionWithPushNotifications(let enabled):
session = UserSessionInfo(id: "1",
name: "macOS",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "1.0.0.1",
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
applicationName: "Element MacOS",
applicationVersion: "1.0.0",
applicationURL: nil,
deviceModel: nil,
deviceOS: "macOS 12.5.1",
lastSeenIPLocation: nil,
deviceName: "My Mac",
isActive: false,
isCurrent: false)
service = MockUserSessionOverviewService(pusherEnabled: enabled)
case .remotelyTogglingPushersNotAvailable:
session = UserSessionInfo(id: "1",
name: "macOS",
deviceType: .desktop,
isVerified: true,
lastSeenIP: "1.0.0.1",
lastSeenTimestamp: Date().timeIntervalSince1970 - 130_000,
applicationName: "Element MacOS",
applicationVersion: "1.0.0",
applicationURL: nil,
deviceModel: nil,
deviceOS: "macOS 12.5.1",
lastSeenIPLocation: nil,
deviceName: "My Mac",
isActive: false,
isCurrent: false)
service = MockUserSessionOverviewService(pusherEnabled: true, remotelyTogglingPushersAvailable: false)
}
let viewModel = UserSessionOverviewViewModel(sessionInfo: session, service: service)
// can simulate service and viewModel actions here if needs be.
return ([viewModel], AnyView(UserSessionOverview(viewModel: viewModel.context)))
}
@@ -0,0 +1,115 @@
//
// 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 Combine
import MatrixSDK
class UserSessionOverviewService: UserSessionOverviewServiceProtocol {
// MARK: - Members
private(set) var pusherEnabledSubject: CurrentValueSubject<Bool?, Never>
private(set) var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
// MARK: - Private
private let session: MXSession
private let sessionInfo: UserSessionInfo
private var pusher: MXPusher?
// MARK: - Setup
init(session: MXSession, sessionInfo: UserSessionInfo) {
self.session = session
self.sessionInfo = sessionInfo
self.pusherEnabledSubject = CurrentValueSubject(nil)
self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(false)
checkServerVersions { [weak self] in
self?.checkPusher()
}
}
// MARK: - UserSessionOverviewServiceProtocol
func togglePushNotifications() {
guard let pusher = pusher, let enabled = pusher.enabled?.boolValue, self.remotelyTogglingPushersAvailableSubject.value else {
return
}
let data = pusher.data.jsonDictionary() as? [String: Any] ?? [:]
self.session.matrixRestClient.setPusher(pushKey: pusher.pushkey,
kind: MXPusherKind(value: pusher.kind),
appId: pusher.appId,
appDisplayName:pusher.appDisplayName,
deviceDisplayName: pusher.deviceDisplayName,
profileTag: pusher.profileTag ?? "",
lang: pusher.lang,
data: data,
append: false,
enabled: !enabled) { [weak self] response in
guard let self = self else { return }
switch response {
case .success:
self.checkPusher()
case .failure(let error):
MXLog.warning("[UserSessionOverviewService] togglePushNotifications failed due to error: \(error)")
self.pusherEnabledSubject.send(enabled)
}
}
}
// MARK: - Private
private func checkServerVersions(_ completion: @escaping () -> Void) {
session.supportedMatrixVersions { [weak self] response in
switch response {
case .success(let versions):
self?.remotelyTogglingPushersAvailableSubject.send(versions.supportsRemotelyTogglingPushNotifications)
case .failure(let error):
MXLog.warning("[UserSessionOverviewService] checkServerVersions failed due to error: \(error)")
}
completion()
}
}
private func checkPusher() {
session.matrixRestClient.pushers { [weak self] response in
switch response {
case .success(let pushers):
self?.check(pushers: pushers)
case .failure(let error):
MXLog.warning("[UserSessionOverviewService] checkPusher failed due to error: \(error)")
}
}
}
private func check(pushers: [MXPusher]) {
for pusher in pushers where pusher.deviceId == sessionInfo.id {
self.pusher = pusher
guard let enabled = pusher.enabled else {
// For backwards compatibility, any pusher without an enabled field should be treated as if enabled is false
pusherEnabledSubject.send(false)
return
}
pusherEnabledSubject.send(enabled.boolValue)
}
}
}
@@ -0,0 +1,38 @@
//
// 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 Combine
import Foundation
class MockUserSessionOverviewService: UserSessionOverviewServiceProtocol {
var pusherEnabledSubject: CurrentValueSubject<Bool?, Never>
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never>
init(pusherEnabled: Bool? = nil, remotelyTogglingPushersAvailable: Bool = true) {
self.pusherEnabledSubject = CurrentValueSubject(pusherEnabled)
self.remotelyTogglingPushersAvailableSubject = CurrentValueSubject(remotelyTogglingPushersAvailable)
}
func togglePushNotifications() {
guard let enabled = pusherEnabledSubject.value, self.remotelyTogglingPushersAvailableSubject.value else {
return
}
pusherEnabledSubject.send(!enabled)
}
}
@@ -0,0 +1,25 @@
//
// 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 Combine
import Foundation
protocol UserSessionOverviewServiceProtocol {
var pusherEnabledSubject: CurrentValueSubject<Bool?, Never> { get }
var remotelyTogglingPushersAvailableSubject: CurrentValueSubject<Bool, Never> { get }
func togglePushNotifications()
}
@@ -34,4 +34,31 @@ class UserSessionOverviewUITests: MockScreenTestCase {
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.currentSession.title)
XCTAssertTrue(app.buttons[VectorL10n.userSessionOverviewSessionDetailsButtonTitle].exists)
}
func test_whenSessionOverviewPresented_pusherEnabledToggleExists() {
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.sessionWithPushNotifications(enabled: true).title)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isOn)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isEnabled)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists)
}
func test_whenSessionOverviewPresented_pusherDisabledToggleExists() {
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.sessionWithPushNotifications(enabled: false).title)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists)
XCTAssertFalse(app.switches["UserSessionOverviewToggleCell"].isOn)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isEnabled)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists)
}
func test_whenSessionOverviewPresented_pusherEnabledToggleExists_remotelyTogglingPushersAvailable() {
app.goToScreenWithIdentifier(MockUserSessionOverviewScreenState.remotelyTogglingPushersNotAvailable.title)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].exists)
XCTAssertTrue(app.switches["UserSessionOverviewToggleCell"].isOn)
XCTAssertFalse(app.switches["UserSessionOverviewToggleCell"].isEnabled)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotifications].exists)
XCTAssertTrue(app.staticTexts[VectorL10n.userSessionPushNotificationsMessage].exists)
}
}
@@ -23,8 +23,9 @@ class UserSessionOverviewViewModelTests: XCTestCase {
var sut: UserSessionOverviewViewModel!
func test_whenVerifyCurrentSessionProcessed_completionWithVerifyCurrentSessionCalled() {
sut = UserSessionOverviewViewModel(session: createUserSessionInfo())
sut = UserSessionOverviewViewModel(sessionInfo: createUserSessionInfo(), service: MockUserSessionOverviewService())
XCTAssertEqual(sut.state.isPusherEnabled, nil)
var modelResult: UserSessionOverviewViewModelResult?
sut.completion = { result in
modelResult = result
@@ -34,17 +35,57 @@ class UserSessionOverviewViewModelTests: XCTestCase {
}
func test_whenViewSessionDetailsProcessed_completionWithShowSessionDetailsCalled() {
let session = createUserSessionInfo()
sut = UserSessionOverviewViewModel(session: session)
let sessionInfo = createUserSessionInfo()
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: MockUserSessionOverviewService())
XCTAssertEqual(sut.state.isPusherEnabled, nil)
var modelResult: UserSessionOverviewViewModelResult?
sut.completion = { result in
modelResult = result
}
sut.process(viewAction: .viewSessionDetails)
XCTAssertEqual(modelResult, .showSessionDetails(session: session))
XCTAssertEqual(modelResult, .showSessionDetails(sessionInfo: sessionInfo))
}
func test_whenViewSessionDetailsProcessed_toggleAvailablePusher() {
let sessionInfo = createUserSessionInfo()
let service = MockUserSessionOverviewService(pusherEnabled: true)
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service)
XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable)
XCTAssertEqual(sut.state.isPusherEnabled, true)
sut.process(viewAction: .togglePushNotifications)
XCTAssertEqual(sut.state.isPusherEnabled, false)
sut.process(viewAction: .togglePushNotifications)
XCTAssertEqual(sut.state.isPusherEnabled, true)
}
func test_whenViewSessionDetailsProcessed_toggleNoPusher() {
let sessionInfo = createUserSessionInfo()
let service = MockUserSessionOverviewService(pusherEnabled: nil)
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service)
XCTAssertTrue(sut.state.remotelyTogglingPushersAvailable)
XCTAssertEqual(sut.state.isPusherEnabled, nil)
sut.process(viewAction: .togglePushNotifications)
XCTAssertEqual(sut.state.isPusherEnabled, nil)
sut.process(viewAction: .togglePushNotifications)
XCTAssertEqual(sut.state.isPusherEnabled, nil)
}
func test_whenViewSessionDetailsProcessed_remotelyTogglingPushersNotAvailable() {
let sessionInfo = createUserSessionInfo()
let service = MockUserSessionOverviewService(pusherEnabled: true, remotelyTogglingPushersAvailable: false)
sut = UserSessionOverviewViewModel(sessionInfo: sessionInfo, service: service)
XCTAssertFalse(sut.state.remotelyTogglingPushersAvailable)
XCTAssertEqual(sut.state.isPusherEnabled, true)
sut.process(viewAction: .togglePushNotifications)
XCTAssertEqual(sut.state.isPusherEnabled, true)
sut.process(viewAction: .togglePushNotifications)
XCTAssertEqual(sut.state.isPusherEnabled, true)
}
private func createUserSessionInfo() -> UserSessionInfo {
UserSessionInfo(id: "session",
name: "iOS",
@@ -52,6 +93,13 @@ class UserSessionOverviewViewModelTests: XCTestCase {
isVerified: false,
lastSeenIP: "10.0.0.10",
lastSeenTimestamp: Date().timeIntervalSince1970 - 100,
applicationName: "Element",
applicationVersion: "1.9.7",
applicationURL: nil,
deviceModel: "iPhone XS",
deviceOS: "iOS 15.5",
lastSeenIPLocation: nil,
deviceName: "Mobile",
isActive: true,
isCurrent: true)
}
@@ -19,13 +19,13 @@ import Foundation
// MARK: - Coordinator
enum UserSessionOverviewCoordinatorResult {
case openSessionDetails(session: UserSessionInfo)
case openSessionDetails(sessionInfo: UserSessionInfo)
}
// MARK: View model
enum UserSessionOverviewViewModelResult: Equatable {
case showSessionDetails(session: UserSessionInfo)
case showSessionDetails(sessionInfo: UserSessionInfo)
case verifyCurrentSession
}
@@ -34,9 +34,13 @@ enum UserSessionOverviewViewModelResult: Equatable {
struct UserSessionOverviewViewState: BindableState {
let cardViewData: UserSessionCardViewData
let isCurrentSession: Bool
var isPusherEnabled: Bool?
var remotelyTogglingPushersAvailable: Bool
var showLoadingIndicator: Bool
}
enum UserSessionOverviewViewAction {
case verifyCurrentSession
case viewSessionDetails
case togglePushNotifications
}
@@ -19,18 +19,45 @@ import SwiftUI
typealias UserSessionOverviewViewModelType = StateStoreViewModel<UserSessionOverviewViewState, UserSessionOverviewViewAction>
class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessionOverviewViewModelProtocol {
private let session: UserSessionInfo
private let sessionInfo: UserSessionInfo
private let service: UserSessionOverviewServiceProtocol
var completion: ((UserSessionOverviewViewModelResult) -> Void)?
init(session: UserSessionInfo) {
self.session = session
// MARK: - Setup
init(sessionInfo: UserSessionInfo, service: UserSessionOverviewServiceProtocol) {
self.sessionInfo = sessionInfo
self.service = service
let cardViewData = UserSessionCardViewData(session: session)
let state = UserSessionOverviewViewState(cardViewData: cardViewData, isCurrentSession: session.isCurrent)
let cardViewData = UserSessionCardViewData(sessionInfo: sessionInfo)
let state = UserSessionOverviewViewState(cardViewData: cardViewData,
isCurrentSession: sessionInfo.isCurrent,
isPusherEnabled: service.pusherEnabledSubject.value,
remotelyTogglingPushersAvailable: service.remotelyTogglingPushersAvailableSubject.value,
showLoadingIndicator: false)
super.init(initialViewState: state)
startObservingService()
}
private func startObservingService() {
service
.pusherEnabledSubject
.sink(receiveValue: { [weak self] pushEnabled in
self?.state.showLoadingIndicator = false
self?.state.isPusherEnabled = pushEnabled
})
.store(in: &cancellables)
service
.remotelyTogglingPushersAvailableSubject
.sink(receiveValue: { [weak self] remotelyTogglingPushersAvailable in
self?.state.remotelyTogglingPushersAvailable = remotelyTogglingPushersAvailable
})
.store(in: &cancellables)
}
// MARK: - Public
override func process(viewAction: UserSessionOverviewViewAction) {
@@ -38,7 +65,10 @@ class UserSessionOverviewViewModel: UserSessionOverviewViewModelType, UserSessio
case .verifyCurrentSession:
completion?(.verifyCurrentSession)
case .viewSessionDetails:
completion?(.showSessionDetails(session: session))
completion?(.showSessionDetails(sessionInfo: sessionInfo))
case .togglePushNotifications:
self.state.showLoadingIndicator = true
service.togglePushNotifications()
}
}
}
@@ -26,7 +26,7 @@ struct UserSessionOverview: View {
UserSessionCardView(viewData: viewModel.viewState.cardViewData, onVerifyAction: { _ in
viewModel.send(viewAction: .verifyCurrentSession)
},
onViewDetailsAction: { _ in
onViewDetailsAction: { _ in
viewModel.send(viewAction: .viewSessionDetails)
})
.padding(16)
@@ -34,13 +34,21 @@ struct UserSessionOverview: View {
UserSessionOverviewDisclosureCell(title: VectorL10n.userSessionOverviewSessionDetailsButtonTitle, onBackgroundTap: {
viewModel.send(viewAction: .viewSessionDetails)
})
if let enabled = viewModel.viewState.isPusherEnabled {
UserSessionOverviewToggleCell(title: VectorL10n.userSessionPushNotifications,
message: VectorL10n.userSessionPushNotificationsMessage,
isOn: enabled, isEnabled: viewModel.viewState.remotelyTogglingPushersAvailable) {
viewModel.send(viewAction: .togglePushNotifications)
}
}
}
}
.background(theme.colors.system.ignoresSafeArea())
.frame(maxHeight: .infinity)
.waitOverlay(show: viewModel.viewState.showLoadingIndicator, allowUserInteraction: false)
.navigationTitle(viewModel.viewState.isCurrentSession ?
VectorL10n.userSessionOverviewCurrentSessionTitle :
VectorL10n.userSessionOverviewSessionTitle)
VectorL10n.userSessionOverviewCurrentSessionTitle :
VectorL10n.userSessionOverviewSessionTitle)
}
}
@@ -33,7 +33,7 @@ struct UserSessionOverviewDisclosureCell: View {
.frame(maxWidth: .infinity, alignment: .leading)
Image(Asset.Images.chevron.name)
}
.padding(.vertical, 12)
.padding(.vertical, 15)
.padding(.horizontal, 16)
SeparatorLine()
}
@@ -0,0 +1,86 @@
//
// 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 UserSessionOverviewToggleCell: View {
@Environment(\.theme) private var theme: ThemeSwiftUI
let title: String
let message: String?
let isOn: Bool
let isEnabled: Bool
var onBackgroundTap: (() -> Void)?
var body: some View {
VStack(spacing: 8) {
Button(action: {
guard isEnabled else { return }
onBackgroundTap?()
}) {
VStack(spacing: 0) {
SeparatorLine()
Toggle(isOn: .constant(isOn)) {
Text(title)
.font(theme.fonts.body)
.foregroundColor(theme.colors.primaryContent)
.opacity(isEnabled ? 1 : 0.3)
}
.disabled(!isEnabled)
.allowsHitTesting(false)
.padding(.vertical, 10)
.padding(.horizontal, 16)
.accessibilityIdentifier("UserSessionOverviewToggleCell")
SeparatorLine()
}
.background(theme.colors.background)
}
.disabled(!isEnabled)
if let message = message {
Text(message)
.multilineTextAlignment(.leading)
.font(theme.fonts.footnote)
.foregroundColor(theme.colors.secondaryContent)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
}
}
}
}
struct UserSessionOverviewToggleCell_Previews: PreviewProvider {
static var previews: some View {
Group {
preview
.theme(.light)
.preferredColorScheme(.light)
preview
.theme(.dark)
.preferredColorScheme(.dark)
}
}
static var preview: some View {
VStack {
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: true, isEnabled: true)
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: false, isEnabled: true)
UserSessionOverviewToggleCell(title: "Title", message: "some very long message text in order to test the multine alignment", isOn: true, isEnabled: true)
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: true, isEnabled: false)
UserSessionOverviewToggleCell(title: "Title", message: nil, isOn: false, isEnabled: false)
UserSessionOverviewToggleCell(title: "Title", message: "some very long message text in order to test the multine alignment", isOn: true, isEnabled: false)
}
}
}