diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index 5625279e0..e3d239e15 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -251,6 +251,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "LabsScreen_Previews") } + func testLeaveSpaceRoomDetailsCell() async throws { + try await performAccessibilityAudit(named: "LeaveSpaceRoomDetailsCell_Previews") + } + func testLeaveSpaceView() async throws { try await performAccessibilityAudit(named: "LeaveSpaceView_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 46ec1ba8e..932d947a0 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -318,6 +318,7 @@ 37906355E207DB5703754675 /* AppLockSetupBiometricsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9F893F4A111CB7BA5C96949 /* AppLockSetupBiometricsScreenViewModel.swift */; }; 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; }; 37E47F5101C0C036289D3807 /* WysiwygComposer in Frameworks */ = {isa = PBXBuildFile; productRef = CA07D57389DACE18AEB6A5E2 /* WysiwygComposer */; }; + 37EE1FB8400BBDC7A7338E57 /* LeaveSpaceRoomDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B329F7962435DB1B5F49F2AC /* LeaveSpaceRoomDetailsCell.swift */; }; 384D6B9A7DFD7260139D6852 /* UITestsNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */; }; 38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; }; 386720B603F87D156DB01FB2 /* VoiceMessageMediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40076C770A5FB83325252973 /* VoiceMessageMediaManager.swift */; }; @@ -2439,6 +2440,7 @@ B2E7C987AE5DC9087BB19F7D /* MediaUploadPreviewScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenModels.swift; sourceTree = ""; }; B2EAFFD44F81F86012D6EC27 /* AudioRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRoomTimelineView.swift; sourceTree = ""; }; B3005886F00029F058DB62BE /* StartChatScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenCoordinator.swift; sourceTree = ""; }; + B329F7962435DB1B5F49F2AC /* LeaveSpaceRoomDetailsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceRoomDetailsCell.swift; sourceTree = ""; }; B343C5255FB408DDE853CFDF /* RoomScreenHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenHook.swift; sourceTree = ""; }; B383DCD3DCB19E00FD478A5F /* ConfirmationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationDialog.swift; sourceTree = ""; }; B4005D82E9D27BAF006A8FE1 /* AppLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModel.swift; sourceTree = ""; }; @@ -6469,6 +6471,7 @@ FA1D480A302295CFC3582543 /* View */ = { isa = PBXGroup; children = ( + B329F7962435DB1B5F49F2AC /* LeaveSpaceRoomDetailsCell.swift */, D7813824C547ED121F6F8E0F /* LeaveSpaceView.swift */, 646B50583A2CE6DA67F7739A /* SpaceScreen.swift */, ); @@ -7882,6 +7885,7 @@ EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */, EFF735EC040BEB669AFBAB50 /* LeaveSpaceHandleProxy.swift in Sources */, DD21CE51DF9BD04FC8155972 /* LeaveSpaceHandleSDKMock.swift in Sources */, + 37EE1FB8400BBDC7A7338E57 /* LeaveSpaceRoomDetailsCell.swift in Sources */, B6CA5D18D702D0919BEF0263 /* LeaveSpaceView.swift in Sources */, 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */, 9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */, diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index ecf1b5066..4b36758e6 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -86,6 +86,7 @@ "action_forgot_password" = "Forgot password?"; "action_forward" = "Forward"; "action_go_back" = "Go back"; +"action_go_to_settings" = "Go to settings"; "action_ignore" = "Ignore"; "action_invite" = "Invite"; "action_invite_friends" = "Invite people"; diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index bed038157..45910ebde 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -206,6 +206,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 settings + internal static var actionGoToSettings: String { return L10n.tr("Localizable", "action_go_to_settings") } /// Ignore internal static var actionIgnore: String { return L10n.tr("Localizable", "action_ignore") } /// Invite diff --git a/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift b/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift index 07f92b710..0fc9002c8 100644 --- a/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift +++ b/ElementX/Sources/Mocks/SDK/LeaveSpaceHandleSDKMock.swift @@ -21,6 +21,16 @@ extension LeaveSpaceHandleSDKMock { } extension [LeaveSpaceRoom] { + static func mockLastSpaceAdmin(spaceRoomProxy: SpaceRoomProxyProtocol) -> [LeaveSpaceRoom] { + mockRooms + [LeaveSpaceRoom(spaceRoom: SpaceRoom(id: spaceRoomProxy.id, + name: spaceRoomProxy.computedName, + avatarURL: spaceRoomProxy.avatarURL, + isSpace: true, + memberCount: UInt64(spaceRoomProxy.joinedMembersCount), + joinRule: spaceRoomProxy.joinRule), + isLastAdmin: true)] + } + static var mockAdminRooms: [LeaveSpaceRoom] { mockRooms.filter(\.isLastAdmin) } diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 8f42406e5..7a0a9b62b 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -70,6 +70,7 @@ enum TestablePreviewsDictionary { "KnockRequestsListEmptyStateView_Previews" : KnockRequestsListEmptyStateView_Previews.self, "KnockRequestsListScreen_Previews" : KnockRequestsListScreen_Previews.self, "LabsScreen_Previews" : LabsScreen_Previews.self, + "LeaveSpaceRoomDetailsCell_Previews" : LeaveSpaceRoomDetailsCell_Previews.self, "LeaveSpaceView_Previews" : LeaveSpaceView_Previews.self, "LegalInformationScreen_Previews" : LegalInformationScreen_Previews.self, "LoadableImage_Previews" : LoadableImage_Previews.self, diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index 080acb937..65f75de1a 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -24,6 +24,8 @@ struct SpaceScreenViewState: BindableState { var selectedSpaceRoomID: String? var joiningRoomIDs: Set = [] + var isSpaceManagementEnabled = false + var bindings = SpaceScreenViewStateBindings() var spaceName: String { space.name ?? L10n.commonSpace } @@ -39,4 +41,5 @@ enum SpaceScreenViewAction { case deselectAllLeaveRoomDetails case toggleLeaveSpaceRoomDetails(id: String) case confirmLeaveSpace + case spaceSettings } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index 8eb91aa96..bfd044570 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -104,6 +104,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc } case .confirmLeaveSpace: Task { await confirmLeaveSpace() } + case .spaceSettings: + break // Not implemented. } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceRoomDetailsCell.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceRoomDetailsCell.swift new file mode 100644 index 000000000..986db339f --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceRoomDetailsCell.swift @@ -0,0 +1,116 @@ +// +// 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 Compound +import SwiftUI + +struct LeaveSpaceRoomDetailsCell: View { + @Environment(\.dynamicTypeSize) private var dynamicTypeSize + + let room: LeaveSpaceRoomDetails + let mediaProvider: MediaProviderProtocol? + + let action: () -> Void + + private var subtitle: String? { + guard !room.spaceRoomProxy.isSpace else { return nil } + let memberCount = L10n.commonMemberCount(room.spaceRoomProxy.joinedMembersCount) + return room.isLastAdmin ? L10n.screenLeaveSpaceLastAdminInfo(memberCount) : memberCount + } + + var visibilityIcon: KeyPath? { + switch room.spaceRoomProxy.visibility { + case .public: \.public + case .private: \.lockSolid + case .restricted: nil + case .none: \.lockSolid + } + } + + var body: some View { + Button(action: action) { + HStack(spacing: 16) { + if dynamicTypeSize < .accessibility3 { + RoomAvatarImage(avatar: room.spaceRoomProxy.avatar, + avatarSize: .room(on: .leaveSpace), + mediaProvider: mediaProvider) + } + + VStack(alignment: .leading, spacing: 0) { + Text(room.spaceRoomProxy.computedName) + .font(.compound.bodyLGSemibold) + .foregroundStyle(.compound.textPrimary) + .lineLimit(1) + .padding(.vertical, 1) + .padding(.vertical, subtitle == nil ? 10 : 0) + + subtitleLabel + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 8) + + ListRowAccessory.multiSelection(room.isSelected) + } + .padding(.horizontal, 16) + } + .buttonStyle(SpaceRoomCellButtonStyle(isSelected: false)) + } + + @ViewBuilder + private var subtitleLabel: some View { + if let subtitle { + Label { + Text(subtitle) + .font(.compound.bodyMD) + .foregroundStyle(.compound.textSecondary) + .lineLimit(1) + .padding(.vertical, 1) + } icon: { + if let visibilityIcon { + CompoundIcon(visibilityIcon, + size: .xSmall, + relativeTo: .compound.bodyMD) + .foregroundStyle(.compound.iconTertiary) + } + } + .labelStyle(.custom(spacing: 4)) + } + } +} + +// MARK: - Previews + +struct LeaveSpaceRoomDetailsCell_Previews: PreviewProvider, TestablePreview { + static var previews: some View { + VStack(spacing: 0) { + LeaveSpaceRoomDetailsCell(room: .init(spaceRoomProxy: SpaceRoomProxyMock(.init(id: "1", + name: "Space", + isSpace: true)), + isLastAdmin: false, + isSelected: true), + mediaProvider: MediaProviderMock(configuration: .init())) { } + LeaveSpaceRoomDetailsCell(room: .init(spaceRoomProxy: SpaceRoomProxyMock(.init(id: "2", + name: "My Space", + isSpace: true)), + isLastAdmin: true, + isSelected: false), + mediaProvider: MediaProviderMock(configuration: .init())) { } + LeaveSpaceRoomDetailsCell(room: .init(spaceRoomProxy: SpaceRoomProxyMock(.init(id: "3", + name: "Room", + isSpace: false)), + isLastAdmin: false, + isSelected: true), + mediaProvider: MediaProviderMock(configuration: .init())) { } + LeaveSpaceRoomDetailsCell(room: .init(spaceRoomProxy: SpaceRoomProxyMock(.init(id: "4", + name: "My Room", + isSpace: false)), + isLastAdmin: true, + isSelected: false), + mediaProvider: MediaProviderMock(configuration: .init())) { } + } + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift index d60430125..da07f6163 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift @@ -9,7 +9,10 @@ import Compound import SwiftUI struct LeaveSpaceView: View { + @Environment(\.dismiss) private var dismiss + let context: SpaceScreenViewModel.Context + let leaveHandle: LeaveSpaceHandleProxy @State private var scrollViewHeight: CGFloat = .zero @State private var buttonsHeight: CGFloat = .zero @@ -39,24 +42,16 @@ struct LeaveSpaceView: View { BigIcon(icon: \.errorSolid, style: .alertSolid) VStack(spacing: 8) { - Text(L10n.screenLeaveSpaceTitle(context.viewState.spaceName)) + Text(leaveHandle.title(spaceName: context.viewState.spaceName)) .font(.compound.headingMDBold) .foregroundStyle(.compound.textPrimary) .multilineTextAlignment(.center) - switch context.leaveHandle?.mode { - case .manyRooms: - Text(L10n.screenLeaveSpaceSubtitle) + if let subtitle = leaveHandle.subtitle { + Text(subtitle) .font(.compound.bodyMD) .foregroundStyle(.compound.textSecondary) .multilineTextAlignment(.center) - case .onlyAdminRooms: - Text(L10n.screenLeaveSpaceSubtitleOnlyLastAdmin) - .font(.compound.bodyMD) - .foregroundStyle(.compound.textSecondary) - .multilineTextAlignment(.center) - case .noRooms, nil: - EmptyView() } } } @@ -65,10 +60,10 @@ struct LeaveSpaceView: View { @ViewBuilder var rooms: some View { - if let leaveRooms = context.leaveHandle?.rooms, !leaveRooms.isEmpty { + if !leaveHandle.rooms.isEmpty, leaveHandle.canLeave { LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) { Section { - ForEach(leaveRooms, id: \.spaceRoomProxy.id) { room in + ForEach(leaveHandle.rooms, id: \.spaceRoomProxy.id) { room in LeaveSpaceRoomDetailsCell(room: room, mediaProvider: context.mediaProvider) { context.send(viewAction: .toggleLeaveSpaceRoomDetails(id: room.spaceRoomProxy.id)) } @@ -90,94 +85,50 @@ struct LeaveSpaceView: View { var buttons: some View { VStack(spacing: 16) { - Button(role: .destructive) { - context.send(viewAction: .confirmLeaveSpace) - } label: { - Label(context.leaveHandle?.confirmationTitle ?? L10n.actionLeaveSpace, icon: \.leave) + if leaveHandle.canLeave { + Button(role: .destructive) { + context.send(viewAction: .confirmLeaveSpace) + } label: { + Label(leaveHandle.confirmationTitle, icon: \.leave) + } + .buttonStyle(.compound(.primary)) + } else if context.viewState.isSpaceManagementEnabled { + Button { + context.send(viewAction: .spaceSettings) + } label: { + Label(L10n.actionGoToSettings, icon: \.settings) + } + .buttonStyle(.compound(.primary)) } - .buttonStyle(.compound(.primary)) - Button(L10n.actionCancel) { - context.leaveHandle = nil - } - .buttonStyle(.compound(.tertiary)) + Button(L10n.actionCancel, action: dismiss.callAsFunction) + .buttonStyle(.compound(.tertiary)) } .padding(.horizontal, 16) .padding(.top, 16) } } -struct LeaveSpaceRoomDetailsCell: View { - @Environment(\.dynamicTypeSize) private var dynamicTypeSize - - let room: LeaveSpaceRoomDetails - let mediaProvider: MediaProviderProtocol? - - let action: () -> Void - - private var subtitle: String? { - guard !room.spaceRoomProxy.isSpace else { return nil } - let memberCount = L10n.commonMemberCount(room.spaceRoomProxy.joinedMembersCount) - return room.isLastAdmin ? L10n.screenLeaveSpaceLastAdminInfo(memberCount) : memberCount - } - - var visibilityIcon: KeyPath? { - switch room.spaceRoomProxy.visibility { - case .public: \.public - case .private: \.lockSolid - case .restricted: nil - case .none: \.lockSolid +private extension LeaveSpaceHandleProxy { + func title(spaceName: String) -> String { + switch mode { + case .lastSpaceAdmin: L10n.screenLeaveSpaceTitleLastAdmin(spaceName) + default: L10n.screenLeaveSpaceTitle(spaceName) } } - var body: some View { - Button(action: action) { - HStack(spacing: 16) { - if dynamicTypeSize < .accessibility3 { - RoomAvatarImage(avatar: room.spaceRoomProxy.avatar, - avatarSize: .room(on: .leaveSpace), - mediaProvider: mediaProvider) - } - - VStack(alignment: .leading, spacing: 0) { - Text(room.spaceRoomProxy.computedName) - .font(.compound.bodyLGSemibold) - .foregroundStyle(.compound.textPrimary) - .lineLimit(1) - .padding(.vertical, 1) - .padding(.vertical, subtitle == nil ? 10 : 0) - - subtitleLabel - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.vertical, 8) - - ListRowAccessory.multiSelection(room.isSelected) - } - .padding(.horizontal, 16) + var subtitle: String? { + switch mode { + case .manyRooms: L10n.screenLeaveSpaceSubtitle + case .onlyAdminRooms: L10n.screenLeaveSpaceSubtitleOnlyLastAdmin + case .noRooms: nil + case .lastSpaceAdmin: L10n.screenLeaveSpaceSubtitleLastAdmin } - .buttonStyle(SpaceRoomCellButtonStyle(isSelected: false)) } - @ViewBuilder - private var subtitleLabel: some View { - if let subtitle { - Label { - Text(subtitle) - .font(.compound.bodyMD) - .foregroundStyle(.compound.textSecondary) - .lineLimit(1) - .padding(.vertical, 1) - } icon: { - if let visibilityIcon { - CompoundIcon(visibilityIcon, - size: .xSmall, - relativeTo: .compound.bodyMD) - .foregroundStyle(.compound.iconTertiary) - } - } - .labelStyle(.custom(spacing: 4)) - } + var confirmationTitle: String { + let selectedCount = selectedCount + return selectedCount > 0 ? L10n.screenLeaveSpaceSubmit(selectedCount) : L10n.actionLeaveSpace } } @@ -186,48 +137,51 @@ struct LeaveSpaceRoomDetailsCell: View { import MatrixRustSDK struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { - static let viewModel = makeViewModel(mode: .manyRooms) - static let onlyAdminRoomsViewModel = makeViewModel(mode: .onlyAdminRooms) - static let noRoomsViewModel = makeViewModel(mode: .noRooms) + static let viewModel = makeViewModel() static var previews: some View { - LeaveSpaceView(context: viewModel.context) + LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .manyRooms)) .previewDisplayName("Many Rooms") - .snapshotPreferences(expect: viewModel.context.observe(\.leaveHandle).map { $0 != nil }.eraseToStream()) - LeaveSpaceView(context: onlyAdminRoomsViewModel.context) + LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .onlyAdminRooms)) .previewDisplayName("Only Admin Rooms") - .snapshotPreferences(expect: viewModel.context.observe(\.leaveHandle).map { $0 != nil }.eraseToStream()) - LeaveSpaceView(context: noRoomsViewModel.context) + LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .noRooms)) .previewDisplayName("No Rooms") - .snapshotPreferences(expect: viewModel.context.observe(\.leaveHandle).map { $0 != nil }.eraseToStream()) + LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .lastSpaceAdmin)) + .previewDisplayName("Last Space Admin") } - static func makeViewModel(mode: LeaveSpaceHandleProxy.Mode) -> SpaceScreenViewModel { - let spaceRoomProxy = SpaceRoomProxyMock(.init(id: "!eng-space:matrix.org", - name: "Engineering Team", - isSpace: true, - parent: SpaceRoomProxyMock(.init(name: "MegaGroup", isSpace: true)), - childrenCount: 30, - joinedMembersCount: 76, - heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose], - topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.", - joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))) + static let spaceRoomProxy = SpaceRoomProxyMock(.init(id: "!eng-space:matrix.org", + name: "Engineering Team", + isSpace: true, + parent: SpaceRoomProxyMock(.init(name: "MegaGroup", isSpace: true)), + childrenCount: 30, + joinedMembersCount: 76, + heroes: [.mockDan, .mockBob, .mockCharlie, .mockVerbose], + topic: "Description of the space goes right here. Lorem ipsum dolor sit amet consectetur. Leo viverra morbi habitant in.", + joinRule: .knockRestricted(rules: [.roomMembership(roomId: "")]))) + + static func makeViewModel() -> SpaceScreenViewModel { let spaceRoomListProxy = SpaceRoomListProxyMock(.init(spaceRoomProxy: spaceRoomProxy, initialSpaceRooms: .mockSpaceList)) - - let rooms: [LeaveSpaceRoom] = switch mode { - case .manyRooms: .mockRooms - case .onlyAdminRooms: .mockAdminRooms - case .noRooms: [] - } - let spaceServiceProxy = SpaceServiceProxyMock(.init(leaveSpaceRooms: rooms)) + let spaceServiceProxy = SpaceServiceProxyMock(.init()) let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy, spaceServiceProxy: spaceServiceProxy, selectedSpaceRoomPublisher: .init(nil), userSession: UserSessionMock(.init()), userIndicatorController: UserIndicatorControllerMock()) - viewModel.context.send(viewAction: .leaveSpace) return viewModel } + + static func makeLeaveHandle(mode: LeaveSpaceHandleProxy.Mode) -> LeaveSpaceHandleProxy { + let rooms: [LeaveSpaceRoom] = switch mode { + case .manyRooms: .mockRooms + case .onlyAdminRooms: .mockAdminRooms + case .noRooms: [] + case .lastSpaceAdmin: .mockLastSpaceAdmin(spaceRoomProxy: spaceRoomProxy) + } + + return LeaveSpaceHandleProxy(spaceID: spaceRoomProxy.id, + leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: rooms))) + } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index f27012884..68e1b52dc 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -23,8 +23,8 @@ struct SpaceScreen: View { .navigationTitle(context.viewState.spaceName) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } - .sheet(item: $context.leaveHandle) { _ in - LeaveSpaceView(context: context) + .sheet(item: $context.leaveHandle) { leaveHandle in + LeaveSpaceView(context: context, leaveHandle: leaveHandle) } } @@ -56,7 +56,8 @@ struct SpaceScreen: View { } ToolbarItemGroup(placement: .secondaryAction) { - if let permalink = context.viewState.permalink { + // FIXME: The ShareLink crashes on iOS 26 due to the share sheet failing to morph out of the button 🤦‍♂️ + if let permalink = context.viewState.permalink, #unavailable(iOS 26.0) { Section { ShareLink(item: permalink) { Label(L10n.actionShare, icon: \.shareIos) diff --git a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift index afa33b931..4a4e1fa1f 100644 --- a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift +++ b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift @@ -10,17 +10,24 @@ import MatrixRustSDK class LeaveSpaceHandleProxy: Identifiable { let id: String - let leaveHandle: LeaveSpaceHandleProtocol var rooms: [LeaveSpaceRoomDetails] - enum Mode { case manyRooms, onlyAdminRooms, noRooms } + enum Mode { case manyRooms, onlyAdminRooms, noRooms, lastSpaceAdmin } let mode: Mode + private let leaveHandle: LeaveSpaceHandleProtocol + + var canLeave: Bool { mode != .lastSpaceAdmin } + var selectedCount: Int { rooms.count { $0.isSelected } } + init(spaceID: String, leaveHandle: LeaveSpaceHandleProtocol) { id = spaceID self.leaveHandle = leaveHandle - rooms = leaveHandle.rooms() + let rooms = leaveHandle.rooms() + let space = rooms.first { $0.spaceRoom.roomId == spaceID } + + self.rooms = rooms .compactMap { room in guard room.spaceRoom.state == .joined, // The SDK is going to do this but not yet. room.spaceRoom.isDirect != true, @@ -32,7 +39,9 @@ class LeaveSpaceHandleProxy: Identifiable { isSelected: !room.isLastAdmin) } - mode = if rooms.isEmpty { + mode = if space?.isLastAdmin == true { + .lastSpaceAdmin + } else if rooms.isEmpty { .noRooms } else if rooms.count(where: { !$0.isLastAdmin }) == 0 { .onlyAdminRooms @@ -54,13 +63,6 @@ class LeaveSpaceHandleProxy: Identifiable { return .failure(.sdkError(error)) } } - - var selectedCount: Int { rooms.count { $0.isSelected } } - - var confirmationTitle: String { - let selectedCount = selectedCount - return selectedCount > 0 ? L10n.screenLeaveSpaceSubmit(selectedCount) : L10n.actionLeaveSpace - } } @Observable class LeaveSpaceRoomDetails { diff --git a/PreviewTests/Sources/GeneratedPreviewTests.swift b/PreviewTests/Sources/GeneratedPreviewTests.swift index 86784068a..4ba407d54 100644 --- a/PreviewTests/Sources/GeneratedPreviewTests.swift +++ b/PreviewTests/Sources/GeneratedPreviewTests.swift @@ -377,6 +377,12 @@ extension PreviewTests { } } + func testLeaveSpaceRoomDetailsCell() async throws { + for (index, preview) in LeaveSpaceRoomDetailsCell_Previews._allPreviews.enumerated() { + try await assertSnapshots(matching: preview, step: index) + } + } + func testLeaveSpaceView() async throws { for (index, preview) in LeaveSpaceView_Previews._allPreviews.enumerated() { try await assertSnapshots(matching: preview, step: index) diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPad-en-GB-0.png new file mode 100644 index 000000000..519af9d0a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPad-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88a74173a12b71340404e5e7ddf643e5b423225dd7e7341ff7999395e85ab29b +size 96983 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPad-pseudo-0.png new file mode 100644 index 000000000..83ed8649b --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPad-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b87616c651ec510de4518fd8257b63e50668e3e0b3a95e3ca32e64d5aa25b0 +size 106567 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPhone-16-en-GB-0.png new file mode 100644 index 000000000..a4babd395 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPhone-16-en-GB-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:265b0cd6386b88ca25348afa117b8e3ba62b6d60c27858481706f62d952f06cf +size 54134 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPhone-16-pseudo-0.png new file mode 100644 index 000000000..095420f78 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceRoomDetailsCell.iPhone-16-pseudo-0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32e0d83b9289cc3d0f0ae904fd3bcd5c0e7694f5417aa3ceb8ef0722aa7c3dbe +size 59145 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-en-GB.png new file mode 100644 index 000000000..ee3e2c638 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c396362920b57958f7e0586fe9161ad7808e91c62e9457c6dbb0379c04b1b488 +size 92482 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-pseudo.png new file mode 100644 index 000000000..0b3f707bd --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3316fbd3a5c7b5310f940bef50381a3a947bfe82c836fe212f3a12d301ccf439 +size 100701 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPhone-16-en-GB.png new file mode 100644 index 000000000..565c10c9f --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4626895535cc88986dc35a8ddb0fac304ca3cc1bf21db3a58daa6efb66912132 +size 52109 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPhone-16-pseudo.png new file mode 100644 index 000000000..b0b234d2e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef1fa43dc064fc2426bfa091eafcf4234f3a85ba2631dd95d5b0dca2648ddac5 +size 70188 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png index 3e23a1db4..99af50963 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bffa459bce40305f442d5babae093b6611dff474e173fe194ab0bb24e3b3c649 -size 163522 +oid sha256:54496c03db4bc1dc6133b06c9d357e63e01871f894a8dba5e01fb155d9ad3378 +size 181927 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-16-pseudo.png index 7d7d3d48e..dcf5c8992 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-16-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Only-Admin-Rooms-iPhone-16-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28db91dd1882c36c5dba6ac2c79727c06d60865eae40226fb4f16981d040a1fa -size 119047 +oid sha256:6a015ff1aa9f4c51efcdb10a4e923f3812c07ef56706fd0912ed4fb90400be33 +size 129184