From fdc741da2ec0aed3930433757cced33e8826b43e Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Wed, 29 Oct 2025 18:59:45 +0100 Subject: [PATCH] Basic implementation of the UI, navigation wil be implemented in the next PR --- .../en.lproj/Localizable.strings | 7 +- .../SpaceSettingsFlowCoordinator.swift | 3 +- ElementX/Sources/Generated/Strings.swift | 12 +- ElementX/Sources/Other/Avatars.swift | 3 +- .../SpaceScreen/SpaceScreenModels.swift | 1 + .../SpaceScreen/SpaceScreenViewModel.swift | 16 +-- .../SpaceScreen/View/LeaveSpaceView.swift | 4 +- .../SpaceSettingsScreenCoordinator.swift | 8 +- .../SpaceSettingsScreenModels.swift | 25 ++-- .../SpaceSettingsScreenViewModel.swift | 82 ++++++++--- .../View/SpaceSettingsScreen.swift | 131 ++++++++++++------ ...bersListScreen.Admin-Banned-iPad-en-GB.png | 4 +- ...ersListScreen.Admin-Banned-iPad-pseudo.png | 4 +- ...istScreen.Admin-Banned-iPhone-16-en-GB.png | 4 +- ...stScreen.Admin-Banned-iPhone-16-pseudo.png | 4 +- ...stScreen.Admin-Empty-Banned-iPad-en-GB.png | 4 +- ...tScreen.Admin-Empty-Banned-iPad-pseudo.png | 4 +- ...een.Admin-Empty-Banned-iPhone-16-en-GB.png | 4 +- ...en.Admin-Empty-Banned-iPhone-16-pseudo.png | 4 +- ...ersListScreen.Admin-Members-iPad-en-GB.png | 4 +- ...rsListScreen.Admin-Members-iPad-pseudo.png | 4 +- ...stScreen.Admin-Members-iPhone-16-en-GB.png | 4 +- ...tScreen.Admin-Members-iPhone-16-pseudo.png | 4 +- ...omMembersListScreen.Invites-iPad-en-GB.png | 4 +- ...mMembersListScreen.Invites-iPad-pseudo.png | 4 +- ...bersListScreen.Invites-iPhone-16-en-GB.png | 4 +- ...ersListScreen.Invites-iPhone-16-pseudo.png | 4 +- ...oomMembersListScreen.Member-iPad-en-GB.png | 4 +- ...omMembersListScreen.Member-iPad-pseudo.png | 4 +- ...mbersListScreen.Member-iPhone-16-en-GB.png | 4 +- ...bersListScreen.Member-iPhone-16-pseudo.png | 4 +- .../spaceSettingsScreen.iPad-en-GB-0.png | 3 + .../spaceSettingsScreen.iPad-pseudo-0.png | 3 + .../spaceSettingsScreen.iPhone-16-en-GB-0.png | 3 + ...spaceSettingsScreen.iPhone-16-pseudo-0.png | 3 + .../Sources/SpaceScreenViewModelTests.swift | 1 + 36 files changed, 250 insertions(+), 135 deletions(-) create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceSettingsScreen.iPad-en-GB-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceSettingsScreen.iPad-pseudo-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceSettingsScreen.iPhone-16-en-GB-0.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/spaceSettingsScreen.iPhone-16-pseudo-0.png diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index 3621b32d9..ce64c26e9 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -87,6 +87,7 @@ "action_forgot_password" = "Forgot password?"; "action_forward" = "Forward"; "action_go_back" = "Go back"; +"action_go_to_roles_and_permissions" = "Go to roles & permissions"; "action_go_to_settings" = "Go to settings"; "action_ignore" = "Ignore"; "action_invite" = "Invite"; @@ -174,7 +175,6 @@ "common_advanced_settings" = "Advanced settings"; "common_an_image" = "an image"; "common_analytics" = "Analytics"; -"common_android_notification_sync_notifications_foreground_service_title" = "Fetching notifications..."; "common_android_shortcuts_remove_reason_left_room" = "You left the room"; "common_android_shortcuts_remove_reason_session_logged_out" = "You were logged out of the session"; "common_appearance" = "Appearance"; @@ -667,6 +667,7 @@ "screen_space_list_details" = "%1$@ • %2$@"; "screen_space_list_parent_space" = "%1$@ space"; "screen_space_list_title" = "Spaces"; +"screen_space_settings_roles_and_permissions" = "Roles & permissions"; "screen_start_chat_join_room_by_address_action" = "Join room by address"; "screen_start_chat_join_room_by_address_invalid_address" = "Not a valid address"; "screen_start_chat_join_room_by_address_placeholder" = "Enter..."; @@ -1053,7 +1054,7 @@ "screen_room_member_details_unblock_user" = "Unblock user"; "screen_room_member_details_verify_button_subtitle" = "Use the web app to verify this user."; "screen_room_member_details_verify_button_title" = "Verify %1$@"; -"screen_room_member_list_banned_empty" = "There are no banned users in this room."; +"screen_room_member_list_banned_empty" = "There are no banned users."; "screen_room_member_list_manage_member_remove_confirmation_kick" = "Only remove member"; "screen_room_member_list_manage_member_unban_action" = "Unban"; "screen_room_member_list_manage_member_unban_message" = "They will be able to join this room again if invited."; @@ -1359,6 +1360,8 @@ "screen_security_and_privacy_room_address_section_header" = "Room address"; "screen_security_and_privacy_room_history_anyone_option_title" = "Anyone"; "screen_security_and_privacy_room_visibility_section_header" = "Room visibility"; +"screen_space_settings_leave_space" = "Leave space"; +"screen_space_settings_security_and_privacy" = "Security & privacy"; "screen_account_provider_change" = "Change account provider"; "screen_account_provider_signin_subtitle" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; "screen_account_provider_signup_subtitle" = "This is where your conversations will live — just like you would use an email provider to keep your emails."; diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index 6bb983924..ae9719fc0 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -87,7 +87,8 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { } private func presentSpaceSettings(animated: Bool) { - let coordinator = SpaceSettingsScreenCoordinator(parameters: .init()) + let coordinator = SpaceSettingsScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userSession: flowParameters.userSession)) coordinator.actionsPublisher.sink { [weak self] action in switch action { } diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index 36db1958a..38d93e442 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -208,6 +208,8 @@ internal enum L10n { internal static var actionForward: String { return L10n.tr("Localizable", "action_forward") } /// Go back internal static var actionGoBack: String { return L10n.tr("Localizable", "action_go_back") } + /// Go to roles & permissions + internal static var actionGoToRolesAndPermissions: String { return L10n.tr("Localizable", "action_go_to_roles_and_permissions") } /// Go to settings internal static var actionGoToSettings: String { return L10n.tr("Localizable", "action_go_to_settings") } /// Ignore @@ -390,8 +392,6 @@ internal enum L10n { internal static var commonAnImage: String { return L10n.tr("Localizable", "common_an_image") } /// Analytics internal static var commonAnalytics: String { return L10n.tr("Localizable", "common_analytics") } - /// Fetching notifications... - internal static var commonAndroidNotificationSyncNotificationsForegroundServiceTitle: String { return L10n.tr("Localizable", "common_android_notification_sync_notifications_foreground_service_title") } /// You left the room internal static var commonAndroidShortcutsRemoveReasonLeftRoom: String { return L10n.tr("Localizable", "common_android_shortcuts_remove_reason_left_room") } /// You were logged out of the session @@ -2464,7 +2464,7 @@ internal enum L10n { internal static func screenRoomMemberDetailsVerifyButtonTitle(_ p1: Any) -> String { return L10n.tr("Localizable", "screen_room_member_details_verify_button_title", String(describing: p1)) } - /// There are no banned users in this room. + /// There are no banned users. internal static var screenRoomMemberListBannedEmpty: String { return L10n.tr("Localizable", "screen_room_member_list_banned_empty") } /// Plural format key: "%#@COUNT@" internal static func screenRoomMemberListHeaderTitle(_ p1: Int) -> String { @@ -2964,6 +2964,12 @@ internal enum L10n { internal static var screenSpaceListTitle: String { return L10n.tr("Localizable", "screen_space_list_title") } /// View members internal static var screenSpaceMenuActionMembers: String { return L10n.tr("Localizable", "screen_space_menu_action_members") } + /// Leave space + internal static var screenSpaceSettingsLeaveSpace: String { return L10n.tr("Localizable", "screen_space_settings_leave_space") } + /// Roles & permissions + internal static var screenSpaceSettingsRolesAndPermissions: String { return L10n.tr("Localizable", "screen_space_settings_roles_and_permissions") } + /// Security & privacy + internal static var screenSpaceSettingsSecurityAndPrivacy: String { return L10n.tr("Localizable", "screen_space_settings_security_and_privacy") } /// An error occurred when trying to start a chat internal static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") } /// Join room by address diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 5be4bf52d..107d728c1 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -134,6 +134,7 @@ enum UserAvatarSizeOnScreen { enum RoomAvatarSizeOnScreen { case chats case spaces + case spaceSettings case timeline case leaveSpace case messageForwarding @@ -148,7 +149,7 @@ enum RoomAvatarSizeOnScreen { var value: CGFloat { switch self { - case .chats, .spaces: + case .chats, .spaces, .spaceSettings: return 52 case .timeline, .leaveSpace: return 32 diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index 003528c41..9be9a72d5 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -45,5 +45,6 @@ enum SpaceScreenViewAction { case toggleLeaveSpaceRoomDetails(id: String) case confirmLeaveSpace case spaceSettings + case rolesAndPermissions case displayMembers(roomProxy: JoinedRoomProxyProtocol) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index a7444c06e..36024f837 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -76,19 +76,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc } 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) - } + .weakAssign(to: \.state.isSpaceManagementEnabled, on: self) .store(in: &cancellables) } } @@ -141,6 +129,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc fatalError("Always available when the space settings button is tapped.") } actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy)) + case .rolesAndPermissions: + break // Not implemented yet } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift index 9e331d279..c0dcb9d9b 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift @@ -99,9 +99,9 @@ struct LeaveSpaceView: View { .buttonStyle(.compound(.primary)) } else if context.viewState.isSpaceManagementEnabled { Button { - context.send(viewAction: .spaceSettings) + context.send(viewAction: .rolesAndPermissions) } label: { - Label(L10n.actionGoToSettings, icon: \.settings) + Label(L10n.actionGoToRolesAndPermissions, icon: \.settings) } .buttonStyle(.compound(.primary)) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift index 749e62da1..335567e64 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift @@ -8,7 +8,10 @@ import Combine import SwiftUI -struct SpaceSettingsScreenCoordinatorParameters { } +struct SpaceSettingsScreenCoordinatorParameters { + let roomProxy: JoinedRoomProxyProtocol + let userSession: UserSessionProtocol +} enum SpaceSettingsScreenCoordinatorAction { } @@ -26,7 +29,8 @@ final class SpaceSettingsScreenCoordinator: CoordinatorProtocol { init(parameters: SpaceSettingsScreenCoordinatorParameters) { self.parameters = parameters - viewModel = SpaceSettingsScreenViewModel() + viewModel = SpaceSettingsScreenViewModel(roomProxy: parameters.roomProxy, + userSession: parameters.userSession) } func start() { diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift index 47779bd4f..145cf08b6 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift @@ -10,23 +10,18 @@ import Foundation enum SpaceSettingsScreenViewModelAction { } struct SpaceSettingsScreenViewState: BindableState { - var title: String - var placeholder: String - var counter = 0 + var details: RoomDetails - var bindings: SpaceSettingsScreenViewStateBindings -} - -struct SpaceSettingsScreenViewStateBindings { - var composerText: String + var joinedMembersCount: Int + var hasMemberIdentityVerificationStateViolations = false + + var canEditRolesOrPermissions = false } enum SpaceSettingsScreenViewAction { - case done - case textChanged - - case incrementCounter - case decrementCounter - - // Consider adding CustomStringConvertible conformance if the actions contain PII + case processTapEdit + case processTapSecurity + case processTapPeople + case processTapRolesAndPermissions + case processTapLeave } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift index 44e967f80..7166760ef 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift @@ -11,15 +11,25 @@ import SwiftUI typealias SpaceSettingsScreenViewModelType = StateStoreViewModelV2 class SpaceSettingsScreenViewModel: SpaceSettingsScreenViewModelType, SpaceSettingsScreenViewModelProtocol { + private let roomProxy: JoinedRoomProxyProtocol + private let userSession: UserSessionProtocol + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - init() { - super.init(initialViewState: SpaceSettingsScreenViewState(title: "SpaceSettings title", - placeholder: "Enter something here", - bindings: .init(composerText: "Initial composer text"))) + init(roomProxy: JoinedRoomProxyProtocol, userSession: UserSessionProtocol) { + self.roomProxy = roomProxy + self.userSession = userSession + + super.init(initialViewState: .init(details: roomProxy.details, + joinedMembersCount: roomProxy.infoPublisher.value.joinedMembersCount), + mediaProvider: userSession.mediaProvider) + + updateRoomInfo(roomProxy.infoPublisher.value) + setupRoomSubscription() + Task { await roomProxy.updateMembers() } } // MARK: - Public @@ -28,20 +38,58 @@ class SpaceSettingsScreenViewModel: SpaceSettingsScreenViewModelType, SpaceSetti MXLog.info("View model: received view action: \(viewAction)") switch viewAction { - case .done: + case .processTapEdit: + break + case .processTapSecurity: + break + case .processTapPeople: + break + case .processTapRolesAndPermissions: + break + case .processTapLeave: 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 - } } } + + private func updateRoomInfo(_ roomInfo: RoomInfoProxyProtocol) { + state.joinedMembersCount = roomInfo.joinedMembersCount + state.details = roomProxy.details + + if let powerLevels = roomInfo.powerLevels { + state.canEditRolesOrPermissions = powerLevels.canOwnUserEditRolesAndPermissions() + } + } + + private func setupRoomSubscription() { + roomProxy.infoPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] roomInfo in + self?.updateRoomInfo(roomInfo) + } + .store(in: &cancellables) + + roomProxy.membersPublisher.combineLatest(roomProxy.identityStatusChangesPublisher) + .sink { [weak self] _ in + Task { await self?.updateMemberIdentityVerificationStates() } + } + .store(in: &cancellables) + } + + private func updateMemberIdentityVerificationStates() async { + guard roomProxy.infoPublisher.value.isEncrypted else { + // We don't care about identity statuses on non-encrypted rooms + return + } + + for member in roomProxy.membersPublisher.value { + if case let .success(userIdentity) = await userSession.clientProxy.userIdentity(for: member.userID) { + if userIdentity?.verificationState == .verificationViolation { + state.hasMemberIdentityVerificationStateViolations = true + return + } + } + } + + state.hasMemberIdentityVerificationStateViolations = false + } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift index 22cddc1e3..e383435be 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift @@ -13,59 +13,112 @@ struct SpaceSettingsScreen: View { 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) }) + editSection + if context.viewState.canEditRolesOrPermissions { + securitySection } + peopleSection + leaveSpaceSection } .compoundList() - .navigationTitle(context.viewState.title) - .onChange(of: context.composerText) { - context.send(viewAction: .textChanged) + .navigationTitle(L10n.commonSettings) + } + + private var editSection: some View { + Section { + ListRow(kind: .custom { + Button { + context.send(viewAction: .processTapEdit) + } label: { + editSectionContent + } + }) } } + + private var editSectionContent: some View { + HStack(spacing: 12) { + RoomAvatarImage(avatar: context.viewState.details.avatar, + avatarSize: .room(on: .spaceSettings), + mediaProvider: context.mediaProvider) + .accessibilityHidden(true) + + VStack(alignment: .leading, spacing: 2) { + Text(context.viewState.details.name ?? context.viewState.details.id) + .lineLimit(1) + .font(.compound.headingMD) + .foregroundStyle(.compound.textPrimary) + if let alias = context.viewState.details.canonicalAlias { + Text(alias) + .lineLimit(1) + .font(.compound.bodySM) + .foregroundStyle(.compound.textSecondary) + } + } + + Spacer() + + ListRowAccessory.navigationLink + } + .padding(.horizontal, ListRowPadding.horizontal) + .padding(.vertical, 16) + } + + private var securitySection: some View { + Section { + ListRow(label: .default(title: L10n.screenSpaceSettingsSecurityAndPrivacy, icon: \.lock), + kind: .navigationLink { + context.send(viewAction: .processTapSecurity) + }) + } + } + + private var peopleSection: some View { + Section { + if context.viewState.hasMemberIdentityVerificationStateViolations { + ListRow(label: .default(title: L10n.commonPeople, icon: \.user), + details: .icon(CompoundIcon(\.infoSolid).foregroundStyle(.compound.iconCriticalPrimary)), + kind: .navigationLink { + context.send(viewAction: .processTapPeople) + }) + } else { + ListRow(label: .default(title: L10n.commonPeople, icon: \.user), + details: .title(String(context.viewState.joinedMembersCount)), + kind: .navigationLink { + context.send(viewAction: .processTapPeople) + }) + } + + if context.viewState.canEditRolesOrPermissions { + ListRow(label: .default(title: L10n.screenSpaceSettingsRolesAndPermissions, icon: \.admin), + kind: .navigationLink { + context.send(viewAction: .processTapRolesAndPermissions) + }) + } + } + } + + private var leaveSpaceSection: some View { + ListRow(label: .action(title: L10n.screenSpaceSettingsLeaveSpace, + icon: \.leave, + role: .destructive), + kind: .button { context.send(viewAction: .processTapLeave) }) + } } // MARK: - Previews struct SpaceSettingsScreen_Previews: PreviewProvider, TestablePreview { - static let viewModel = makeViewModel() - static let incrementedViewModel = makeViewModel(counterValue: 1) + static let viewModel = SpaceSettingsScreenViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Space", + avatarURL: .mockMXCAvatar, + isSpace: true, + canonicalAlias: "#space:matrix.org", + members: .allMembersAsCreator)), + userSession: UserSessionMock(.init())) 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..