implemented the basics of the flow coordinator, the logic and the navigation flow to get to the space settings view
This commit is contained in:
@@ -65,6 +65,7 @@ final class AppSettings {
|
||||
case developerOptionsEnabled
|
||||
case linkPreviewsEnabled
|
||||
case latestEventSorterEnabled
|
||||
case spaceSettingsEnabled
|
||||
|
||||
// Doug's tweaks 🔧
|
||||
case hideUnreadMessagesBadge
|
||||
@@ -388,6 +389,9 @@ final class AppSettings {
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var threadsEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.spaceSettingsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var spaceSettingsEnabled
|
||||
|
||||
@UserPreference(key: UserDefaultsKeys.linkPreviewsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||
var linkPreviewsEnabled
|
||||
|
||||
@@ -42,6 +42,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private var childSpaceFlowCoordinator: SpaceFlowCoordinator?
|
||||
private var roomFlowCoordinator: RoomFlowCoordinator?
|
||||
private var membersFlowCoordinator: RoomMembersFlowCoordinator?
|
||||
private var settingsFlowCoordinator: SpaceSettingsFlowCoordinator?
|
||||
|
||||
indirect enum State: StateType {
|
||||
/// The state machine hasn't started.
|
||||
@@ -56,6 +57,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case roomFlow(previousState: State)
|
||||
/// A members flow is in progress
|
||||
case membersFlow
|
||||
/// A space settings flow is in progress
|
||||
case settingsFlow
|
||||
|
||||
case leftSpace
|
||||
}
|
||||
@@ -83,6 +86,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
case startMembersFlow
|
||||
case stopMembersFlow
|
||||
|
||||
case startSettingsFlow
|
||||
case stopSettingsFlow
|
||||
}
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
@@ -142,6 +148,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
case .membersFlow:
|
||||
membersFlowCoordinator?.clearRoute(animated: animated)
|
||||
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
||||
case .settingsFlow:
|
||||
settingsFlowCoordinator?.clearRoute(animated: animated)
|
||||
clearRoute(animated: animated) // Re-run with the state machine back in the .space state.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +221,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else {
|
||||
fatalError("The room proxy must always be provided")
|
||||
}
|
||||
Task { await self.startMembersFlow(roomProxy: roomProxy) }
|
||||
startMembersFlow(roomProxy: roomProxy)
|
||||
}
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
@@ -223,6 +232,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
membersFlowCoordinator = nil
|
||||
}
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
guard event == .startSettingsFlow, case .space = fromState else { return nil }
|
||||
return .settingsFlow
|
||||
} handler: { [weak self] context in
|
||||
guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return }
|
||||
startSettingsFlow(roomProxy: roomProxy)
|
||||
}
|
||||
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
guard event == .stopSettingsFlow, case .settingsFlow = fromState else { return nil }
|
||||
return .space
|
||||
} handler: { [weak self] _ in
|
||||
guard let self else { return }
|
||||
settingsFlowCoordinator = nil
|
||||
}
|
||||
|
||||
stateMachine.addErrorHandler { context in
|
||||
fatalError("Unexpected transition: \(context)")
|
||||
}
|
||||
@@ -235,6 +260,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
spaceServiceProxy: spaceServiceProxy,
|
||||
selectedSpaceRoomPublisher: selectedSpaceRoomSubject.asCurrentValuePublisher(),
|
||||
userSession: flowParameters.userSession,
|
||||
appSettings: flowParameters.appSettings,
|
||||
userIndicatorController: flowParameters.userIndicatorController)
|
||||
let coordinator = SpaceScreenCoordinator(parameters: parameters)
|
||||
coordinator.actionsPublisher
|
||||
@@ -251,6 +277,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
stateMachine.tryEvent(.leftSpace)
|
||||
case .displayMembers(let roomProxy):
|
||||
stateMachine.tryEvent(.startMembersFlow, userInfo: roomProxy)
|
||||
case .displaySpaceSettings(roomProxy: let roomProxy):
|
||||
stateMachine.tryEvent(.startSettingsFlow, userInfo: roomProxy)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
@@ -372,7 +400,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
selectedSpaceRoomSubject.send(roomID)
|
||||
}
|
||||
|
||||
private func startMembersFlow(roomProxy: JoinedRoomProxyProtocol) async {
|
||||
private func startMembersFlow(roomProxy: JoinedRoomProxyProtocol) {
|
||||
let flowCoordinator = RoomMembersFlowCoordinator(entryPoint: .roomMembersList,
|
||||
roomProxy: roomProxy,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
@@ -393,4 +421,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol {
|
||||
membersFlowCoordinator = flowCoordinator
|
||||
flowCoordinator.start()
|
||||
}
|
||||
|
||||
private func startSettingsFlow(roomProxy: JoinedRoomProxyProtocol) {
|
||||
let flowCoordinator = SpaceSettingsFlowCoordinator(roomProxy: roomProxy,
|
||||
navigationStackCoordinator: navigationStackCoordinator,
|
||||
flowParameters: flowParameters)
|
||||
|
||||
flowCoordinator.actions.sink { [weak self] actions in
|
||||
guard let self else { return }
|
||||
switch actions {
|
||||
case .finished:
|
||||
stateMachine.tryEvent(.stopSettingsFlow)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
settingsFlowCoordinator = flowCoordinator
|
||||
flowCoordinator.start()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// Copyright 2025 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftState
|
||||
|
||||
enum SpaceSettingsFlowCoordinatorAction {
|
||||
case finished
|
||||
}
|
||||
|
||||
final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
indirect enum State: StateType {
|
||||
/// The state machine hasn't started.
|
||||
case initial
|
||||
/// The space settings screen
|
||||
case spaceSettings
|
||||
}
|
||||
|
||||
enum Event: EventType {
|
||||
case start
|
||||
|
||||
case presentSpaceSettings
|
||||
}
|
||||
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let navigationStackCoordinator: NavigationStackCoordinator
|
||||
private let flowParameters: CommonFlowParameters
|
||||
|
||||
private let stateMachine: StateMachine<State, Event>
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SpaceSettingsFlowCoordinatorAction, Never> = .init()
|
||||
var actions: AnyPublisher<SpaceSettingsFlowCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(roomProxy: JoinedRoomProxyProtocol,
|
||||
navigationStackCoordinator: NavigationStackCoordinator,
|
||||
flowParameters: CommonFlowParameters) {
|
||||
self.roomProxy = roomProxy
|
||||
self.flowParameters = flowParameters
|
||||
self.navigationStackCoordinator = navigationStackCoordinator
|
||||
|
||||
stateMachine = .init(state: .initial)
|
||||
configureStateMachine()
|
||||
}
|
||||
|
||||
func start(animated: Bool) {
|
||||
stateMachine.tryEvent(.presentSpaceSettings, userInfo: animated)
|
||||
}
|
||||
|
||||
func handleAppRoute(_ appRoute: AppRoute, animated: Bool) {
|
||||
fatalError("Not implemented yet")
|
||||
}
|
||||
|
||||
func clearRoute(animated: Bool) {
|
||||
// Not implemented yet
|
||||
}
|
||||
|
||||
private func configureStateMachine() {
|
||||
stateMachine.addRouteMapping { event, fromState, _ in
|
||||
switch (fromState, event) {
|
||||
case (.initial, .presentSpaceSettings):
|
||||
return .spaceSettings
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
stateMachine.addAnyHandler(.any => .any) { [weak self] context in
|
||||
guard let self else { return }
|
||||
let animated = context.userInfo as? Bool ?? true
|
||||
switch (context.fromState, context.event, context.toState) {
|
||||
case (.initial, .presentSpaceSettings, .spaceSettings):
|
||||
presentSpaceSettings(animated: animated)
|
||||
|
||||
default:
|
||||
fatalError("Unhandled transition")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentSpaceSettings(animated: Bool) {
|
||||
let coordinator = SpaceSettingsScreenCoordinator(parameters: .init())
|
||||
|
||||
coordinator.actionsPublisher.sink { [weak self] action in
|
||||
switch action { }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in
|
||||
self?.actionsSubject.send(.finished)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,7 @@ enum TestablePreviewsDictionary {
|
||||
"SpaceListScreen_Previews" : SpaceListScreen_Previews.self,
|
||||
"SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self,
|
||||
"SpaceScreen_Previews" : SpaceScreen_Previews.self,
|
||||
"SpaceSettingsScreen_Previews" : SpaceSettingsScreen_Previews.self,
|
||||
"SpacesAnnouncementSheetView_Previews" : SpacesAnnouncementSheetView_Previews.self,
|
||||
"SplashScreen_Previews" : SplashScreen_Previews.self,
|
||||
"StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self,
|
||||
|
||||
@@ -54,6 +54,8 @@ protocol DeveloperOptionsProtocol: AnyObject {
|
||||
var latestEventSorterEnabled: Bool { get set }
|
||||
|
||||
var linkPreviewsEnabled: Bool { get set }
|
||||
|
||||
var spaceSettingsEnabled: Bool { get set }
|
||||
}
|
||||
|
||||
extension AppSettings: DeveloperOptionsProtocol { }
|
||||
|
||||
@@ -33,6 +33,12 @@ struct DeveloperOptionsScreen: View {
|
||||
}
|
||||
}
|
||||
|
||||
Section("Spaces") {
|
||||
Toggle(isOn: $context.spaceSettingsEnabled) {
|
||||
Text("Space settings")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Room List") {
|
||||
Toggle(isOn: $context.publicSearchEnabled) {
|
||||
Text("Public search")
|
||||
|
||||
@@ -16,6 +16,7 @@ struct SpaceScreenCoordinatorParameters {
|
||||
let spaceServiceProxy: SpaceServiceProxyProtocol
|
||||
let selectedSpaceRoomPublisher: CurrentValuePublisher<String?, Never>
|
||||
let userSession: UserSessionProtocol
|
||||
let appSettings: AppSettings
|
||||
let userIndicatorController: UserIndicatorControllerProtocol
|
||||
}
|
||||
|
||||
@@ -25,6 +26,7 @@ enum SpaceScreenCoordinatorAction {
|
||||
case selectRoom(roomID: String)
|
||||
case leftSpace
|
||||
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||
case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol)
|
||||
}
|
||||
|
||||
final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
@@ -45,6 +47,7 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
spaceServiceProxy: parameters.spaceServiceProxy,
|
||||
selectedSpaceRoomPublisher: parameters.selectedSpaceRoomPublisher,
|
||||
userSession: parameters.userSession,
|
||||
appSettings: parameters.appSettings,
|
||||
userIndicatorController: parameters.userIndicatorController)
|
||||
}
|
||||
|
||||
@@ -64,6 +67,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol {
|
||||
actionsSubject.send(.leftSpace)
|
||||
case .displayMembers(let roomProxy):
|
||||
actionsSubject.send(.displayMembers(roomProxy: roomProxy))
|
||||
case .displaySpaceSettings(let roomProxy):
|
||||
actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy))
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
@@ -14,6 +14,7 @@ enum SpaceScreenViewModelAction {
|
||||
case selectRoom(roomID: String)
|
||||
case leftSpace
|
||||
case displayMembers(roomProxy: JoinedRoomProxyProtocol)
|
||||
case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol)
|
||||
}
|
||||
|
||||
struct SpaceScreenViewState: BindableState {
|
||||
|
||||
@@ -15,6 +15,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
private let spaceRoomListProxy: SpaceRoomListProxyProtocol
|
||||
private let spaceServiceProxy: SpaceServiceProxyProtocol
|
||||
private let clientProxy: ClientProxyProtocol
|
||||
private let appSettings: AppSettings
|
||||
private let userIndicatorController: UserIndicatorControllerProtocol
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SpaceScreenViewModelAction, Never> = .init()
|
||||
@@ -26,11 +27,13 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
spaceServiceProxy: SpaceServiceProxyProtocol,
|
||||
selectedSpaceRoomPublisher: CurrentValuePublisher<String?, Never>,
|
||||
userSession: UserSessionProtocol,
|
||||
appSettings: AppSettings,
|
||||
userIndicatorController: UserIndicatorControllerProtocol) {
|
||||
self.spaceRoomListProxy = spaceRoomListProxy
|
||||
self.spaceServiceProxy = spaceServiceProxy
|
||||
clientProxy = userSession.clientProxy
|
||||
self.userIndicatorController = userIndicatorController
|
||||
self.appSettings = appSettings
|
||||
|
||||
super.init(initialViewState: SpaceScreenViewState(space: spaceRoomListProxy.spaceRoomProxyPublisher.value,
|
||||
rooms: spaceRoomListProxy.spaceRoomsPublisher.value,
|
||||
@@ -63,10 +66,6 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
selectedSpaceRoomPublisher
|
||||
.weakAssign(to: \.state.selectedSpaceRoomID, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) {
|
||||
// Required to listen for membership updates in the members flow
|
||||
@@ -75,6 +74,22 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
if case let .success(permalinkURL) = await roomProxy.matrixToPermalink() {
|
||||
state.permalink = permalinkURL
|
||||
}
|
||||
|
||||
appSettings.$spaceSettingsEnabled
|
||||
.combineLatest(roomProxy.infoPublisher)
|
||||
.sink { [weak self] isEnabled, info in
|
||||
guard let self else { return }
|
||||
guard isEnabled, let powerLevels = info.powerLevels else {
|
||||
state.isSpaceManagementEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
state.isSpaceManagementEnabled = powerLevels.canOwnUserEditRolesAndPermissions() ||
|
||||
powerLevels.canOwnUser(sendStateEvent: .roomName) ||
|
||||
powerLevels.canOwnUser(sendStateEvent: .roomTopic) ||
|
||||
powerLevels.canOwnUser(sendStateEvent: .roomAvatar)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,7 +137,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
case .displayMembers(let roomProxy):
|
||||
actionsSubject.send(.displayMembers(roomProxy: roomProxy))
|
||||
case .spaceSettings:
|
||||
break // Not implemented.
|
||||
guard let roomProxy = state.roomProxy else {
|
||||
fatalError("Always available when the space settings button is tapped.")
|
||||
}
|
||||
actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +197,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePermissions() { }
|
||||
|
||||
// MARK: - Indicators
|
||||
|
||||
private static var leavingIndicatorID: String { "\(Self.self)-Leaving" }
|
||||
|
||||
@@ -173,6 +173,7 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview {
|
||||
spaceServiceProxy: spaceServiceProxy,
|
||||
selectedSpaceRoomPublisher: .init(nil),
|
||||
userSession: UserSessionMock(.init()),
|
||||
appSettings: AppSettings(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@@ -73,6 +73,12 @@ struct SpaceScreen: View {
|
||||
Label(L10n.actionShare, icon: \.shareIos)
|
||||
}
|
||||
}
|
||||
|
||||
if context.viewState.isSpaceManagementEnabled {
|
||||
Button { context.send(viewAction: .spaceSettings) } label: {
|
||||
Label(L10n.commonSettings, icon: \.settings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -121,6 +127,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview {
|
||||
spaceServiceProxy: SpaceServiceProxyMock(.init()),
|
||||
selectedSpaceRoomPublisher: .init(nil),
|
||||
userSession: userSession,
|
||||
appSettings: AppSettings(),
|
||||
userIndicatorController: UserIndicatorControllerMock())
|
||||
return viewModel
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct SpaceSettingsScreenCoordinatorParameters { }
|
||||
|
||||
enum SpaceSettingsScreenCoordinatorAction { }
|
||||
|
||||
final class SpaceSettingsScreenCoordinator: CoordinatorProtocol {
|
||||
private let parameters: SpaceSettingsScreenCoordinatorParameters
|
||||
private let viewModel: SpaceSettingsScreenViewModelProtocol
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let actionsSubject: PassthroughSubject<SpaceSettingsScreenCoordinatorAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<SpaceSettingsScreenCoordinatorAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init(parameters: SpaceSettingsScreenCoordinatorParameters) {
|
||||
self.parameters = parameters
|
||||
|
||||
viewModel = SpaceSettingsScreenViewModel()
|
||||
}
|
||||
|
||||
func start() {
|
||||
viewModel.actionsPublisher.sink { [weak self] action in
|
||||
MXLog.info("Coordinator: received view model action: \(action)")
|
||||
|
||||
guard let self else { return }
|
||||
switch action { }
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(SpaceSettingsScreen(context: viewModel.context))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum SpaceSettingsScreenViewModelAction { }
|
||||
|
||||
struct SpaceSettingsScreenViewState: BindableState {
|
||||
var title: String
|
||||
var placeholder: String
|
||||
var counter = 0
|
||||
|
||||
var bindings: SpaceSettingsScreenViewStateBindings
|
||||
}
|
||||
|
||||
struct SpaceSettingsScreenViewStateBindings {
|
||||
var composerText: String
|
||||
}
|
||||
|
||||
enum SpaceSettingsScreenViewAction {
|
||||
case done
|
||||
case textChanged
|
||||
|
||||
case incrementCounter
|
||||
case decrementCounter
|
||||
|
||||
// Consider adding CustomStringConvertible conformance if the actions contain PII
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
typealias SpaceSettingsScreenViewModelType = StateStoreViewModelV2<SpaceSettingsScreenViewState, SpaceSettingsScreenViewAction>
|
||||
|
||||
class SpaceSettingsScreenViewModel: SpaceSettingsScreenViewModelType, SpaceSettingsScreenViewModelProtocol {
|
||||
private let actionsSubject: PassthroughSubject<SpaceSettingsScreenViewModelAction, Never> = .init()
|
||||
var actionsPublisher: AnyPublisher<SpaceSettingsScreenViewModelAction, Never> {
|
||||
actionsSubject.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
init() {
|
||||
super.init(initialViewState: SpaceSettingsScreenViewState(title: "SpaceSettings title",
|
||||
placeholder: "Enter something here",
|
||||
bindings: .init(composerText: "Initial composer text")))
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
override func process(viewAction: SpaceSettingsScreenViewAction) {
|
||||
MXLog.info("View model: received view action: \(viewAction)")
|
||||
|
||||
switch viewAction {
|
||||
case .done:
|
||||
break
|
||||
case .textChanged:
|
||||
MXLog.info("View model: composer text changed to: \(state.bindings.composerText)")
|
||||
case .incrementCounter:
|
||||
Task {
|
||||
try await Task.sleep(for: .seconds(.random(in: 1.0...2.0)))
|
||||
state.counter += 1
|
||||
}
|
||||
case .decrementCounter:
|
||||
Task {
|
||||
try await Task.sleep(for: .seconds(.random(in: 1.0...2.0)))
|
||||
state.counter -= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
protocol SpaceSettingsScreenViewModelProtocol {
|
||||
var actionsPublisher: AnyPublisher<SpaceSettingsScreenViewModelAction, Never> { get }
|
||||
var context: SpaceSettingsScreenViewModelType.Context { get }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// Copyright 2025 Element Creations Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
// Please see LICENSE files in the repository root for full details.
|
||||
//
|
||||
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
struct SpaceSettingsScreen: View {
|
||||
@Bindable var context: SpaceSettingsScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
ListRow(label: .plain(title: context.viewState.placeholder),
|
||||
kind: .textField(text: $context.composerText))
|
||||
|
||||
ListRow(label: .centeredAction(title: L10n.actionDone,
|
||||
icon: \.leave),
|
||||
kind: .button { context.send(viewAction: .done) })
|
||||
}
|
||||
|
||||
Section {
|
||||
ListRow(label: .default(title: "Counter", icon: \.chart),
|
||||
details: .counter(context.viewState.counter),
|
||||
kind: .label)
|
||||
ListRow(label: .default(title: "Increment", icon: \.plus),
|
||||
kind: .button { context.send(viewAction: .incrementCounter) })
|
||||
ListRow(label: .default(title: "Decrement", icon: \.minus),
|
||||
kind: .button { context.send(viewAction: .decrementCounter) })
|
||||
}
|
||||
}
|
||||
.compoundList()
|
||||
.navigationTitle(context.viewState.title)
|
||||
.onChange(of: context.composerText) {
|
||||
context.send(viewAction: .textChanged)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct SpaceSettingsScreen_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = makeViewModel()
|
||||
static let incrementedViewModel = makeViewModel(counterValue: 1)
|
||||
|
||||
static var previews: some View {
|
||||
NavigationStack {
|
||||
SpaceSettingsScreen(context: viewModel.context)
|
||||
}
|
||||
.previewDisplayName("Initial")
|
||||
|
||||
NavigationStack {
|
||||
SpaceSettingsScreen(context: incrementedViewModel.context)
|
||||
}
|
||||
.previewDisplayName("Incremented")
|
||||
.snapshotPreferences(expect: incrementedViewModel.context.observe(\.viewState.counter).map { $0 == 1 }.eraseToStream())
|
||||
}
|
||||
|
||||
static func makeViewModel(counterValue: Int = 0) -> SpaceSettingsScreenViewModel {
|
||||
let viewModel = SpaceSettingsScreenViewModel()
|
||||
|
||||
for _ in 0..<counterValue {
|
||||
viewModel.context.send(viewAction: .incrementCounter)
|
||||
}
|
||||
|
||||
return viewModel
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user