From 6b19d109c722957ecdc63b197d8fc2692728fe17 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:11:21 +0100 Subject: [PATCH] Space Settings: Leave Room (#4700) * Implementation for all navigations inside the space settings aside the left space action # Conflicts: # ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift * refactored the leave space view to use its own view model # Conflicts: # ElementX.xcodeproj/project.pbxproj * implemented the leave space view model also in the settings screen, and corrected some tests * reusing the details coordinator for the space settings screen * leave space from settings implemented * fix project * minor pr fixes * code improvements --- ElementX.xcodeproj/project.pbxproj | 121 ++++++++++-------- .../SpaceFlowCoordinator.swift | 51 +++++++- .../SpaceSettingsFlowCoordinator.swift | 36 ++++-- .../RoomDetailsScreenCoordinator.swift | 8 +- .../RoomDetailsScreenModels.swift | 3 + .../RoomDetailsScreenViewModel.swift | 27 +++- .../Spaces/LeaveSpace/LeaveSpaceModels.swift | 48 +++++++ .../LeaveSpace/LeaveSpaceViewModel.swift | 79 ++++++++++++ .../View/LeaveSpaceRoomDetailsCell.swift | 0 .../View/LeaveSpaceView.swift | 88 +++++-------- .../SpaceScreen/SpaceScreenCoordinator.swift | 3 + .../SpaceScreen/SpaceScreenModels.swift | 8 +- .../SpaceScreen/SpaceScreenViewModel.swift | 75 ++++------- .../Spaces/SpaceScreen/View/SpaceScreen.swift | 4 +- .../SpaceSettingsScreenCoordinator.swift | 76 ----------- .../View/SpaceSettingsScreen.swift | 3 + .../Spaces/LeaveSpaceHandleProxy.swift | 21 ++- ...eSpaceView.Last-Space-Admin-iPad-en-GB.png | 4 +- ...SpaceView.Last-Space-Admin-iPad-pseudo.png | 4 +- ...eView.Last-Space-Admin-iPhone-16-en-GB.png | 4 +- ...View.Last-Space-Admin-iPhone-16-pseudo.png | 4 +- .../Sources/SpaceScreenViewModelTests.swift | 17 +-- 22 files changed, 407 insertions(+), 277 deletions(-) create mode 100644 ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift create mode 100644 ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift rename ElementX/Sources/Screens/Spaces/{SpaceScreen => LeaveSpace}/View/LeaveSpaceRoomDetailsCell.swift (100%) rename ElementX/Sources/Screens/Spaces/{SpaceScreen => LeaveSpace}/View/LeaveSpaceView.swift (60%) delete mode 100644 ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index a366495fc..baf1c0e68 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 77; + objectVersion = 63; objects = { /* Begin PBXAggregateTarget section */ @@ -583,6 +583,7 @@ 68B2DD307C57ECFABBB05323 /* DeclineAndBlockScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127D1947BA9C6CA62E3D03EC /* DeclineAndBlockScreen.swift */; }; 68C3AF257678F6E7BB238C3F /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FD22AEFFA20065494ED2333 /* AppAppearance.swift */; }; 695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; }; + 695BE6A2337A634F48B5DBC8 /* RoomMembersFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC666DAE98245269775329B2 /* RoomMembersFlowCoordinatorTests.swift */; }; 69A9B430397C15075D86193F /* UserPropertiesExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66AFD800AF033D8B0D11191A /* UserPropertiesExt.swift */; }; 69B3C6010B42010F591FC3CB /* RoomRolesAndPermissionsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C1AF829F12FDC99717082D9 /* RoomRolesAndPermissionsScreenViewModel.swift */; }; 69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; }; @@ -944,10 +945,10 @@ A6F345328CCC5C9B0DAE2257 /* LogViewerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB05221D7D941CC82DC8480 /* LogViewerScreenViewModel.swift */; }; A6FFC4C5154C446BAD6B40D8 /* TimelineItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520AFD6680CBAD388F6D927 /* TimelineItemProvider.swift */; }; A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; + A7385BAC2EBDFF80000B0961 /* LeaveSpaceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7385BAB2EBDFF7B000B0961 /* LeaveSpaceModels.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7DB75E090542331F6668A23 /* CreateRoomScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */; }; - A7F6CE532EBB76A500450B70 /* RoomMembersFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7F6CE522EBB769D00450B70 /* RoomMembersFlowCoordinatorTests.swift */; }; A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; A851635B3255C6DC07034A12 /* RoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8108C8F0ACF6A7EB72D0117 /* RoomScreenCoordinator.swift */; }; @@ -1337,6 +1338,7 @@ F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; }; F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; }; F2E580C0FBFBEFFE9D69893B /* RoomPreviewProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */; }; + F3382570701BB87DA816AC82 /* LeaveSpaceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C59748CC34606FF4FE95F0 /* LeaveSpaceViewModel.swift */; }; F34D06F86C29E219E7132E87 /* AuthenticationStartScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F71A54CB96DAA1E72C6541D /* AuthenticationStartScreenViewModel.swift */; }; F35FAD1B1B289E221A07D719 /* ManageRoomMemberSheetViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB7B588A06911B455AC0B4C9 /* ManageRoomMemberSheetViewModelProtocol.swift */; }; F37629BAA5E8F50AAF2A131D /* SoftLogoutScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB7BAD55A4E2B8E5828CD64C /* SoftLogoutScreenViewModel.swift */; }; @@ -1399,7 +1401,6 @@ FCD3F2B82CAB29A07887A127 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DE8DC9B3FBA402117DC4C49F /* Kingfisher */; }; FCF95603F1D056B1B106A415 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83E2B20431F890ED64255CA1 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; FD29471C72872F8B7580E3E1 /* KeychainControllerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */; }; - FD3C94F01ACAF2D4948CF9BE /* SpaceSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEA9DA018160CF76AFFBFBA7 /* SpaceSettingsScreenCoordinator.swift */; }; FD439E183A48BE871AEEFAEA /* TimelineScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E10765FBC83B34A3BC4ADB23 /* TimelineScrollToBottomButton.swift */; }; FD4C21F8DA1E273DE94FCD1A /* NotificationItemProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B927CF5EF7FCCDA5EDC474B /* NotificationItemProxyProtocol.swift */; }; FD573B5D665824EB79EABF06 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5327E3B3C58BEB0E65F4CF98 /* Observable.swift */; }; @@ -1523,7 +1524,7 @@ 044E501B8331B339874D1B96 /* CompoundIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompoundIcon.swift; sourceTree = ""; }; 045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = ""; }; 046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryTests.swift; sourceTree = ""; }; - 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = ""; }; 04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = ""; }; @@ -1603,7 +1604,7 @@ 128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = ""; }; 12B09A94C519227264A41208 /* RoomMembershipDetailsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxy.swift; sourceTree = ""; }; 12FD5280AF55AB7F50F8E47D /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -1623,7 +1624,7 @@ 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = ""; }; 16D353E10A64172D863769BF /* TombstonedAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TombstonedAvatarImage.swift; sourceTree = ""; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = ""; }; - 174E4AEF3DED300AA81046EC /* compound-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "compound-ios"; path = "compound-ios"; sourceTree = SOURCE_ROOT; }; + 174E4AEF3DED300AA81046EC /* compound-ios */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "compound-ios"; sourceTree = SOURCE_ROOT; }; 17A8AA0DFA06012A9DAB951E /* TimelineProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineProxyMock.swift; sourceTree = ""; }; 17BAE25A0E9E9F2F1BBA8930 /* DeactivateAccountScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreenViewModel.swift; sourceTree = ""; }; 181CF280BC8E3F335AFCB4B8 /* RemotePreferenceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemotePreferenceTests.swift; sourceTree = ""; }; @@ -1713,7 +1714,7 @@ 25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = ""; }; 25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = ""; }; 260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = ""; }; - 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = ""; }; + 267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = ""; }; 26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = ""; }; 26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = ""; }; 2711E5996016ABD6EAAEB58A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; @@ -1796,7 +1797,7 @@ 355C8C46DA9C0B45F1B7FC4F /* SpaceRoomProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomProxy.swift; sourceTree = ""; }; 35A057BA9BE0F079784CD061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = ""; }; 3747C96188856006F784BF49 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; @@ -1909,7 +1910,7 @@ 4A2B5274C1D3D2999D643786 /* EncryptionResetPasswordScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModelProtocol.swift; sourceTree = ""; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; 4AB29A2D95D3469B5F016655 /* SecureBackupControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupControllerMock.swift; sourceTree = ""; }; - 4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; path = AppIcon.icon; sourceTree = ""; }; + 4B1F71AC585827E6C416C15A /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; }; 4B2B564CA6570E1487A7C7CC /* SpaceRoomListProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceRoomListProxy.swift; sourceTree = ""; }; 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = ""; }; 4B41FABA2B0AEF4389986495 /* LoginMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginMode.swift; sourceTree = ""; }; @@ -1923,7 +1924,7 @@ 4E2245243369B99216C7D84E /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = ""; }; 4E600B315B920B9687F8EE1B /* ComposerDraftServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceMock.swift; sourceTree = ""; }; 4E625B0EB2F86B37C14EF7E6 /* SettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenViewModel.swift; sourceTree = ""; }; - 4E6690212271866C899AD2BA /* new-message.caf */ = {isa = PBXFileReference; path = "new-message.caf"; sourceTree = ""; }; + 4E6690212271866C899AD2BA /* new-message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "new-message.caf"; sourceTree = ""; }; 4E7F7A975514E850A834B29F /* PaginationIndicatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineView.swift; sourceTree = ""; }; 4F5F0662483ED69791D63B16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = et; path = et.lproj/Localizable.stringsdict; sourceTree = ""; }; 4F75EF13F49DD2204E760910 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = ""; }; @@ -2240,7 +2241,7 @@ 8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; 8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = ""; }; 8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = ""; }; 8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = ""; }; @@ -2362,6 +2363,7 @@ A6B891A6DA826E2461DBB40F /* PHGPostHogConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHGPostHogConfiguration.swift; sourceTree = ""; }; A6C11AD9813045E44F950410 /* ElementCallWidgetDriverProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriverProtocol.swift; sourceTree = ""; }; A6EA0D8B0BBD8805F7D5A133 /* TextBasedRoomTimelineViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextBasedRoomTimelineViewProtocol.swift; sourceTree = ""; }; + A7385BAB2EBDFF7B000B0961 /* LeaveSpaceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceModels.swift; sourceTree = ""; }; A73A07BAEDD74C48795A996A /* AsyncSequence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncSequence.swift; sourceTree = ""; }; A768CA51A59B8A5D8C8FD599 /* AuthenticationStartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreen.swift; sourceTree = ""; }; A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = ""; }; @@ -2369,7 +2371,6 @@ A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = ""; }; A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = ""; }; A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = ""; }; - A7F6CE522EBB769D00450B70 /* RoomMembersFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinatorTests.swift; sourceTree = ""; }; A84D413BF49F0E980F010A6B /* LogViewerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenCoordinator.swift; sourceTree = ""; }; A8558D41DD4B553A752C868A /* StackedAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackedAvatarsView.swift; sourceTree = ""; }; A861DA5932B128FE1DCB5CE2 /* InviteUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenCoordinator.swift; sourceTree = ""; }; @@ -2387,7 +2388,7 @@ AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationAnnotation.swift; sourceTree = ""; }; AB07F03461023BC39C730922 /* PhishingDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetector.swift; sourceTree = ""; }; AB26D5444A4A7E095222DE8B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = ""; }; - AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; path = AccessibilityTests.xctestplan; sourceTree = ""; }; + AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AccessibilityTests.xctestplan; sourceTree = ""; }; ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = ""; }; AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = ""; }; AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = ""; }; @@ -2455,7 +2456,7 @@ B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5D829FD8958376614504B18 /* TargetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetConfiguration.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = ""; }; B65DDCF8E41759890355ACBC /* AuthenticationStartScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenViewModelProtocol.swift; sourceTree = ""; }; B682FE2C44C5E163E7023B05 /* CopyTextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyTextButton.swift; sourceTree = ""; }; @@ -2478,6 +2479,7 @@ B858A61F2A570DFB8DE570A7 /* AggregratedReaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregratedReaction.swift; sourceTree = ""; }; B88CE0A058727BC68EEEC6B6 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; B8A3B7637DDBD6AA97AC2545 /* CameraPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraPicker.swift; sourceTree = ""; }; + B8C59748CC34606FF4FE95F0 /* LeaveSpaceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeaveSpaceViewModel.swift; sourceTree = ""; }; B8F28602AC7AC881AED37EBA /* NavigationCoordinators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationCoordinators.swift; sourceTree = ""; }; B902EA6CD3296B0E10EE432B /* HomeScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenModels.swift; sourceTree = ""; }; @@ -2485,7 +2487,7 @@ BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = ""; }; BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = ""; }; BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = ""; }; - BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = ""; }; + BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_apple_image.heic; sourceTree = ""; }; BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = ""; }; BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = ""; }; @@ -2579,6 +2581,7 @@ CC03209FDE8CE0810617BFFF /* RoomMembersListScreenMemberCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenMemberCell.swift; sourceTree = ""; }; CC1DDB2293A51EA4C2739351 /* RoomListFiltersEmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersEmptyStateView.swift; sourceTree = ""; }; CC437C491EA6996513B1CEAB /* ElementCallConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallConfiguration.swift; sourceTree = ""; }; + CC666DAE98245269775329B2 /* RoomMembersFlowCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersFlowCoordinatorTests.swift; sourceTree = ""; }; CC680E0E79D818706CB28CF8 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; CC743C7A85E3171BCBF0A653 /* AvatarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarHeaderView.swift; sourceTree = ""; }; CCF71646898A2F720C5BFDF5 /* RoomDirectorySearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenViewModel.swift; sourceTree = ""; }; @@ -2589,7 +2592,7 @@ CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; CF19027E7FFA5E63D148873A /* CreateRoomScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomScreenViewModel.swift; sourceTree = ""; }; CF847A34FC4C8C937CD39E08 /* LabsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabsScreenViewModelProtocol.swift; sourceTree = ""; }; CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxy.swift; sourceTree = ""; }; @@ -2658,7 +2661,7 @@ DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = ""; }; DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = ""; }; DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = ""; }; - DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = ""; }; + DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = ""; }; DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = ""; }; DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenTests.swift; sourceTree = ""; }; DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = ""; }; @@ -2705,7 +2708,7 @@ E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = ""; }; E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = ""; }; - E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = ""; }; + E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = ""; }; E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = ""; }; E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModel.swift; sourceTree = ""; }; @@ -2752,13 +2755,12 @@ ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = ""; }; ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = ""; }; EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceFlowCoordinator.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; EE6BFF453838CF6C3982C5A3 /* RoomDirectorySearchScreenScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenViewModelTests.swift; sourceTree = ""; }; - EEA9DA018160CF76AFFBFBA7 /* SpaceSettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreenCoordinator.swift; sourceTree = ""; }; EEAB5662310AE73D93815134 /* JoinRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenViewModelProtocol.swift; sourceTree = ""; }; EF13BFD415CA84B1272E94F8 /* PINTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PINTextFieldTests.swift; sourceTree = ""; }; EF1593DD87F974F8509BB619 /* ElementAnimations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementAnimations.swift; sourceTree = ""; }; @@ -4253,7 +4255,6 @@ 55312ACF4155CC5B2054AD75 /* SpaceSettingsScreen */ = { isa = PBXGroup; children = ( - EEA9DA018160CF76AFFBFBA7 /* SpaceSettingsScreenCoordinator.swift */, 7C19E3A92E016D6E126DB06D /* View */, ); path = SpaceSettingsScreen; @@ -4579,7 +4580,6 @@ 73CD9796729EB702B4DFA88C /* Sources */ = { isa = PBXGroup; children = ( - A7F6CE522EBB769D00450B70 /* RoomMembersFlowCoordinatorTests.swift */, 58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */, C687844F60BFF532D49A994C /* AnalyticsTests.swift */, E461B3C8BBBFCA400B268D14 /* AppRouteURLParserTests.swift */, @@ -4653,6 +4653,7 @@ 4FCB2126C091EEF2454B4D56 /* RoomFlowCoordinatorTests.swift */, 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */, EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */, + CC666DAE98245269775329B2 /* RoomMembersFlowCoordinatorTests.swift */, 3E9E0929CEFA356090BE5FB8 /* RoomMembersListScreenViewModelTests.swift */, 58D295F0081084F38DB20893 /* RoomNotificationSettingsScreenViewModelTests.swift */, F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */, @@ -5323,7 +5324,9 @@ 985411D514A5913EB1B60B54 /* Spaces */ = { isa = PBXGroup; children = ( + A7385BA92EBDFF31000B0961 /* LeaveSpace */, BDDD421CD80AD0BCBA035076 /* Common */, + ADA672F2647D05F087305B21 /* LeaveSpaceSheet */, FCF165F4DDB83F3DECFEB57A /* SpaceListScreen */, C360FCF7418FE3593D5A0CBF /* SpaceScreen */, 55312ACF4155CC5B2054AD75 /* SpaceSettingsScreen */, @@ -5523,6 +5526,25 @@ path = View; sourceTree = ""; }; + A7385BA92EBDFF31000B0961 /* LeaveSpace */ = { + isa = PBXGroup; + children = ( + A7385BAB2EBDFF7B000B0961 /* LeaveSpaceModels.swift */, + B8C59748CC34606FF4FE95F0 /* LeaveSpaceViewModel.swift */, + A7385BAA2EBDFF42000B0961 /* View */, + ); + path = LeaveSpace; + sourceTree = ""; + }; + A7385BAA2EBDFF42000B0961 /* View */ = { + isa = PBXGroup; + children = ( + D7813824C547ED121F6F8E0F /* LeaveSpaceView.swift */, + B329F7962435DB1B5F49F2AC /* LeaveSpaceRoomDetailsCell.swift */, + ); + path = View; + sourceTree = ""; + }; A7694BCE812C4D7B2B1B42DF /* View */ = { isa = PBXGroup; children = ( @@ -5619,6 +5641,13 @@ path = View; sourceTree = ""; }; + ADA672F2647D05F087305B21 /* LeaveSpaceSheet */ = { + isa = PBXGroup; + children = ( + ); + path = LeaveSpaceSheet; + sourceTree = ""; + }; B04B538A859CD012755DC19C /* NSE */ = { isa = PBXGroup; children = ( @@ -5848,6 +5877,13 @@ path = MapLibre; sourceTree = ""; }; + C18958141C8ED6D778F779A4 /* CreateRoom */ = { + isa = PBXGroup; + children = ( + ); + path = CreateRoom; + sourceTree = ""; + }; C1CD278862878F9545608040 /* SessionVerificationScreen */ = { isa = PBXGroup; children = ( @@ -6230,6 +6266,7 @@ 53FB148CD26AFB6A5B9E20B3 /* BugReportScreen */, 1185EECDD07495D65AC84AFC /* CallScreen */, 90DC2E28718955ED87AD1456 /* CreatePollScreen */, + C18958141C8ED6D778F779A4 /* CreateRoom */, 821EB0D1C0019E3C7BBAEDBB /* CreateRoomScreen */, 3E1CCC4B607946CE90B4A827 /* DeclineAndBlockScreen */, 45F2BCFD6E9A6F040CC20582 /* EditRoomAddressScreen */, @@ -6486,8 +6523,6 @@ FA1D480A302295CFC3582543 /* View */ = { isa = PBXGroup; children = ( - B329F7962435DB1B5F49F2AC /* LeaveSpaceRoomDetailsCell.swift */, - D7813824C547ED121F6F8E0F /* LeaveSpaceView.swift */, 646B50583A2CE6DA67F7739A /* SpaceScreen.swift */, ); path = View; @@ -6964,7 +6999,6 @@ EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */, C89CF7729E028671C5DC461E /* XCLocalSwiftPackageReference "compound-ios" */, ); - preferredProjectObjectVersion = 77; projectDirPath = ""; projectRoot = ""; targets = ( @@ -7376,7 +7410,6 @@ 3582056513A384F110EC8274 /* MediaPlayerProviderTests.swift in Sources */, 167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */, B9A8C34A00D03094C0CF56F3 /* MediaUploadPreviewScreenViewModelTests.swift in Sources */, - A7F6CE532EBB76A500450B70 /* RoomMembersFlowCoordinatorTests.swift in Sources */, 23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */, F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */, 1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */, @@ -7409,6 +7442,7 @@ 095D3906CF2F940C2D2D17CC /* RoomFlowCoordinatorTests.swift in Sources */, 4C8C0C9FC10BA73AB7780534 /* RoomListFiltersStateTests.swift in Sources */, 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */, + 695BE6A2337A634F48B5DBC8 /* RoomMembersFlowCoordinatorTests.swift in Sources */, 5D56CE09743C6B90C21B04C2 /* RoomMembersListScreenViewModelTests.swift in Sources */, E49F74BD93230BDEFFE5EA51 /* RoomNotificationSettingsScreenViewModelTests.swift in Sources */, 2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */, @@ -7591,6 +7625,7 @@ 0F6C8033FA60CFD36F7CA205 /* AppLockSetupPINScreenViewModel.swift in Sources */, 4C356F5CCB4CDC99BFA45185 /* AppLockSetupPINScreenViewModelProtocol.swift in Sources */, 4E8A2A2CFEB212F14E49E1A1 /* AppLockSetupSettingsScreen.swift in Sources */, + A7385BAC2EBDFF80000B0961 /* LeaveSpaceModels.swift in Sources */, 8DCD9CC5361FF22A5B2C20F1 /* AppLockSetupSettingsScreenCoordinator.swift in Sources */, 6E4E401BE97AC241DA7C7716 /* AppLockSetupSettingsScreenModels.swift in Sources */, 4807E8F51DB54F56B25E1C7E /* AppLockSetupSettingsScreenViewModel.swift in Sources */, @@ -7895,6 +7930,7 @@ DD21CE51DF9BD04FC8155972 /* LeaveSpaceHandleSDKMock.swift in Sources */, 37EE1FB8400BBDC7A7338E57 /* LeaveSpaceRoomDetailsCell.swift in Sources */, B6CA5D18D702D0919BEF0263 /* LeaveSpaceView.swift in Sources */, + F3382570701BB87DA816AC82 /* LeaveSpaceViewModel.swift in Sources */, 42B084FDE621FBEE433AF444 /* LegalInformationScreen.swift in Sources */, 9EBDC79CAC9B63A0D626E333 /* LegalInformationScreenCoordinator.swift in Sources */, F40B097470D3110DFDB1FAAA /* LegalInformationScreenModels.swift in Sources */, @@ -8329,7 +8365,6 @@ DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */, D0E257557DAC8A34C7B52A9F /* SpaceSettingsFlowCoordinator.swift in Sources */, 383063A7924F06D54BA9B24C /* SpaceSettingsScreen.swift in Sources */, - FD3C94F01ACAF2D4948CF9BE /* SpaceSettingsScreenCoordinator.swift in Sources */, 9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */, DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */, E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */, @@ -8784,9 +8819,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DRELEASE", - ); + OTHER_SWIFT_FLAGS = "-DRELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.accessibility.tests"; PRODUCT_NAME = AccessibilityTests; SDKROOT = iphoneos; @@ -8805,9 +8838,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DDEBUG", - ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.accessibility.tests"; PRODUCT_NAME = AccessibilityTests; SDKROOT = iphoneos; @@ -8829,9 +8860,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; @@ -8880,9 +8909,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -8908,9 +8935,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_MAIN_APP", - ); + OTHER_SWIFT_FLAGS = "-DIS_MAIN_APP"; PILLS_UT_TYPE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER).pills"; PRODUCT_BUNDLE_IDENTIFIER = "$(BASE_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(APP_NAME)"; @@ -9135,9 +9160,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DDEBUG", - ); + OTHER_SWIFT_FLAGS = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -9156,9 +9179,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - OTHER_SWIFT_FLAGS = ( - "-DRELEASE", - ); + OTHER_SWIFT_FLAGS = "-DRELEASE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.ui.tests"; PRODUCT_NAME = UITests; SDKROOT = iphoneos; @@ -9180,9 +9201,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = "$(MARKETING_VERSION)"; - OTHER_SWIFT_FLAGS = ( - "-DIS_NSE", - ); + OTHER_SWIFT_FLAGS = "-DIS_NSE"; PRODUCT_BUNDLE_IDENTIFIER = "${BASE_BUNDLE_IDENTIFIER}.nse"; PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)"; PRODUCT_NAME = NSE; diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index e66495b73..3780d02fa 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -43,6 +43,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { private var roomFlowCoordinator: RoomFlowCoordinator? private var membersFlowCoordinator: RoomMembersFlowCoordinator? private var settingsFlowCoordinator: SpaceSettingsFlowCoordinator? + private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator? indirect enum State: StateType { /// The state machine hasn't started. @@ -60,6 +61,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { /// A space settings flow is in progress case settingsFlow + case rolesAndPermissionsFlow + case leftSpace } @@ -89,6 +92,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case startSettingsFlow case stopSettingsFlow + + case startRolesAndPermissionsFlow + case stopRolesAndPermissionsFlow } private let stateMachine: StateMachine @@ -151,6 +157,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case .settingsFlow: settingsFlowCoordinator?.clearRoute(animated: animated) clearRoute(animated: animated) // Re-run with the state machine back in the .space state. + case .rolesAndPermissionsFlow: + rolesAndPermissionsFlowCoordinator?.clearRoute(animated: animated) + clearRoute(animated: animated) // Re-run with the state machine back in the .space state. } } @@ -248,6 +257,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { settingsFlowCoordinator = nil } + stateMachine.addRouteMapping { event, fromState, _ in + guard event == .startRolesAndPermissionsFlow, case .space = fromState else { return nil } + return .rolesAndPermissionsFlow + } handler: { [weak self] context in + guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } + startRolesAndPermissionsFlow(roomProxy: roomProxy) + } + + stateMachine.addRouteMapping { event, fromState, _ in + guard event == .stopRolesAndPermissionsFlow, case .rolesAndPermissionsFlow = fromState else { return nil } + return .space + } handler: { [weak self] _ in + guard let self else { return } + rolesAndPermissionsFlowCoordinator = nil + } + stateMachine.addErrorHandler { context in fatalError("Unexpected transition: \(context)") } @@ -279,6 +304,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.startMembersFlow, userInfo: roomProxy) case .displaySpaceSettings(let roomProxy): stateMachine.tryEvent(.startSettingsFlow, userInfo: roomProxy) + case .displayRolesAndPermissions(let roomProxy): + stateMachine.tryEvent(.startRolesAndPermissionsFlow, userInfo: roomProxy) } } .store(in: &cancellables) @@ -430,8 +457,11 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { flowCoordinator.actions.sink { [weak self] actions in guard let self else { return } switch actions { - case .finished: + case .finished(let leftRoom): stateMachine.tryEvent(.stopSettingsFlow) + if leftRoom { + stateMachine.tryEvent(.leftSpace) + } case .presentCallScreen(let roomProxy): actionsSubject.send(.presentCallScreen(roomProxy: roomProxy)) case .verifyUser(userID: let userID): @@ -443,4 +473,23 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { settingsFlowCoordinator = flowCoordinator flowCoordinator.start() } + + private func startRolesAndPermissionsFlow(roomProxy: JoinedRoomProxyProtocol) { + let flowCoordinator = RoomRolesAndPermissionsFlowCoordinator(parameters: .init(roomProxy: roomProxy, + mediaProvider: flowParameters.userSession.mediaProvider, + navigationStackCoordinator: navigationStackCoordinator, + userIndicatorController: flowParameters.userIndicatorController, + analytics: flowParameters.analytics)) + flowCoordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .complete: + stateMachine.tryEvent(.stopRolesAndPermissionsFlow) + } + } + .store(in: &cancellables) + + rolesAndPermissionsFlowCoordinator = flowCoordinator + flowCoordinator.start() + } } diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index e0289737e..146f023d0 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -10,7 +10,7 @@ import Foundation import SwiftState enum SpaceSettingsFlowCoordinatorAction { - case finished + case finished(leftRoom: Bool) case presentCallScreen(roomProxy: JoinedRoomProxyProtocol) case verifyUser(userID: String) } @@ -65,6 +65,9 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { private let stateMachine: StateMachine private var cancellables = Set() + private var membersFlowCoordinator: RoomMembersFlowCoordinator? + private var rolesAndPermissionsFlowCoordinator: RoomRolesAndPermissionsFlowCoordinator? + private var childFlowCoordinator: FlowCoordinatorProtocol? private let actionsSubject: PassthroughSubject = .init() @@ -178,31 +181,40 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { } private func presentSpaceSettings(animated: Bool) { - let coordinator = SpaceSettingsScreenCoordinator(parameters: .init(roomProxy: roomProxy, - userSession: flowParameters.userSession, - analyticsService: flowParameters.analytics, - userIndicator: flowParameters.userIndicatorController, - notificationSettingsProxy: flowParameters.userSession.clientProxy.notificationSettings, - attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), - appSettings: flowParameters.appSettings)) + let coordinator = RoomDetailsScreenCoordinator(parameters: .init(roomProxy: roomProxy, + userSession: flowParameters.userSession, + analyticsService: flowParameters.analytics, + userIndicatorController: flowParameters.userIndicatorController, + notificationSettings: flowParameters.userSession.clientProxy.notificationSettings, + attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), + appSettings: flowParameters.appSettings)) - coordinator.actionsPublisher.sink { [weak self] action in + var leftRoom = false + coordinator.actions.sink { [weak self] action in guard let self else { return } switch action { - case .presentEditDetailsScreen: + case .presentRoomDetailsEditScreen: stateMachine.tryEvent(.presentEditDetailsScreen) case .presentSecurityAndPrivacyScreen: stateMachine.tryEvent(.presentSecurityAndPrivacyScreen) - case .presentMembersListScreen: + case .presentRoomMembersList: stateMachine.tryEvent(.startMembersListFlow) case .presentRolesAndPermissionsScreen: stateMachine.tryEvent(.startRolesAndPermissionsFlow) + case .leftRoom: + leftRoom = true + navigationStackCoordinator.pop() + case .presentRecipientDetails, .presentNotificationSettingsScreen, .transferOwnership, + .presentInviteUsersScreen, .presentPollsHistory, .presentCall, + .presentPinnedEventsTimeline, .presentMediaEventsTimeline, .presentKnockingRequestsListScreen, + .presentReportRoomScreen: + fatalError("Not handled in the space context") } } .store(in: &cancellables) navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in - self?.actionsSubject.send(.finished) + self?.actionsSubject.send(.finished(leftRoom: leftRoom)) } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index cd4847d12..39ceb6395 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -39,6 +39,7 @@ enum RoomDetailsScreenCoordinatorAction { final class RoomDetailsScreenCoordinator: CoordinatorProtocol { private var viewModel: RoomDetailsScreenViewModelProtocol + private let isSpace: Bool private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() @@ -48,6 +49,7 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { } init(parameters: RoomDetailsScreenCoordinatorParameters) { + isSpace = parameters.roomProxy.infoPublisher.value.isSpace viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy, userSession: parameters.userSession, analyticsService: parameters.analyticsService, @@ -105,6 +107,10 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { } func toPresentable() -> AnyView { - AnyView(RoomDetailsScreen(context: viewModel.context)) + if isSpace { + AnyView(SpaceSettingsScreen(context: viewModel.context)) + } else { + AnyView(RoomDetailsScreen(context: viewModel.context)) + } } } diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 27ce3805b..d9d77339c 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -172,6 +172,9 @@ struct RoomDetailsScreenViewStateBindings { /// A media item that will be previewed with QuickLook. var mediaPreviewItem: MediaPreviewItem? + + /// The view model used to display the leave space sheet, will only be used if the room is a space. + var leaveSpaceViewModel: LeaveSpaceViewModel? } struct LeaveRoomAlertItem: AlertProtocol { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 7f9755989..c65262278 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -209,10 +209,29 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr private func processLeaveSpace() async { switch await userSession.clientProxy.spaceService.leaveSpace(spaceID: roomProxy.id) { - case .success: - // TODO: Handle leave space - break - case .failure(let failure): + case .success(let leaveHandle): + let leaveSpaceViewModel = LeaveSpaceViewModel(spaceName: state.details.name ?? state.details.id, + canEditRolesAndPermissions: state.canEditRolesOrPermissions, + leaveHandle: leaveHandle, + userIndicatorController: userIndicatorController, + mediaProvider: userSession.mediaProvider) + leaveSpaceViewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .didCancel: + state.bindings.leaveSpaceViewModel = nil + case .presentRolesAndPermissions: + state.bindings.leaveSpaceViewModel = nil + actionsSubject.send(.requestRolesAndPermissionsPresentation) + case .didLeaveSpace: + state.bindings.leaveSpaceViewModel = nil + actionsSubject.send(.leftRoom) + } + } + .store(in: &cancellables) + + state.bindings.leaveSpaceViewModel = leaveSpaceViewModel + case .failure: userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) } } diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift new file mode 100644 index 000000000..1ffd404d7 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceModels.swift @@ -0,0 +1,48 @@ +// +// 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. +// + +enum LeaveSpaceViewAction { + case confirmLeaveSpace + case deselectAll + case selectAll + case toggleRoom(roomID: String) + case rolesAndPermissions + case cancel +} + +struct LeaveSpaceViewState: BindableState { + let spaceName: String + let canEditRolesAndPermissions: Bool + let leaveHandle: LeaveSpaceHandleProxy + + var title: String { + switch leaveHandle.mode { + case .lastSpaceAdmin: L10n.screenLeaveSpaceTitleLastAdmin(spaceName) + default: L10n.screenLeaveSpaceTitle(spaceName) + } + } + + var subtitle: String? { + switch leaveHandle.mode { + case .manyRooms: L10n.screenLeaveSpaceSubtitle + case .onlyAdminRooms: L10n.screenLeaveSpaceSubtitleOnlyLastAdmin + case .noRooms: nil + case .lastSpaceAdmin: L10n.screenLeaveSpaceSubtitleLastAdmin + } + } + + var confirmationTitle: String { + let selectedCount = leaveHandle.selectedCount + return selectedCount > 0 ? L10n.screenLeaveSpaceSubmit(selectedCount) : L10n.actionLeaveSpace + } +} + +enum LeaveSpaceViewModelAction { + case didLeaveSpace + case presentRolesAndPermissions + case didCancel +} diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift new file mode 100644 index 000000000..3c092465d --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift @@ -0,0 +1,79 @@ +// +// 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 LeaveSpaceViewModelType = StateStoreViewModelV2 + +class LeaveSpaceViewModel: LeaveSpaceViewModelType { + let actionsSubject = PassthroughSubject() + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } + private let userIndicatorController: UserIndicatorControllerProtocol + private let mediaProvider: MediaProviderProtocol + + init(spaceName: String, canEditRolesAndPermissions: Bool, leaveHandle: LeaveSpaceHandleProxy, userIndicatorController: UserIndicatorControllerProtocol, mediaProvider: MediaProviderProtocol) { + self.userIndicatorController = userIndicatorController + self.mediaProvider = mediaProvider + super.init(initialViewState: LeaveSpaceViewState(spaceName: spaceName, canEditRolesAndPermissions: canEditRolesAndPermissions, leaveHandle: leaveHandle), mediaProvider: mediaProvider) + } + + override func process(viewAction: LeaveSpaceViewAction) { + switch viewAction { + case .confirmLeaveSpace: + Task { await confirmLeaveSpace() } + case .rolesAndPermissions: + actionsSubject.send(.presentRolesAndPermissions) + case .cancel: + actionsSubject.send(.didCancel) + case .deselectAll: + state.leaveHandle.deselectAll() + case .selectAll: + state.leaveHandle.selectAll() + case .toggleRoom(let roomID): + withTransaction(\.disablesAnimations, true) { // The button is adding an unwanted animation. + state.leaveHandle.toggleRoom(roomID: roomID) + } + } + } + + private func confirmLeaveSpace() async { + showLeavingIndicator() + defer { hideLeavingIndicator() } + + switch await state.leaveHandle.leave() { + case .success: + actionsSubject.send(.didLeaveSpace) + case .failure: + showFailureIndicator() + } + } + + private static var leavingIndicatorID: String { "\(Self.self)-Leaving" } + private static var failureIndicatorID: String { "\(Self.self)-Failure" } + + private func showLeavingIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.leavingIndicatorID, + type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), + title: L10n.commonLeavingSpace)) + } + + private func hideLeavingIndicator() { + userIndicatorController.retractIndicatorWithId(Self.leavingIndicatorID) + } + + private func showFailureIndicator() { + userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID, + type: .toast, + title: L10n.errorUnknown, + iconName: "xmark")) + } +} + +extension LeaveSpaceViewModel: Identifiable { + var id: String { state.leaveHandle.id } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceRoomDetailsCell.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceRoomDetailsCell.swift similarity index 100% rename from ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceRoomDetailsCell.swift rename to ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceRoomDetailsCell.swift diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift similarity index 60% rename from ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift rename to ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift index 920d78881..5a4a89ea4 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/View/LeaveSpaceView.swift @@ -12,8 +12,7 @@ import SwiftUI struct LeaveSpaceView: View { @Environment(\.dismiss) private var dismiss - let context: SpaceScreenViewModel.Context - let leaveHandle: LeaveSpaceHandleProxy + let context: LeaveSpaceViewModel.Context @State private var scrollViewHeight: CGFloat = .zero @State private var buttonsHeight: CGFloat = .zero @@ -43,12 +42,12 @@ struct LeaveSpaceView: View { BigIcon(icon: \.errorSolid, style: .alertSolid) VStack(spacing: 8) { - Text(leaveHandle.title(spaceName: context.viewState.space.name)) + Text(context.viewState.title) .font(.compound.headingMDBold) .foregroundStyle(.compound.textPrimary) .multilineTextAlignment(.center) - if let subtitle = leaveHandle.subtitle { + if let subtitle = context.viewState.subtitle { Text(subtitle) .font(.compound.bodyMD) .foregroundStyle(.compound.textSecondary) @@ -61,21 +60,22 @@ struct LeaveSpaceView: View { @ViewBuilder var rooms: some View { - if !leaveHandle.rooms.isEmpty, leaveHandle.canLeave { + if !context.viewState.leaveHandle.rooms.isEmpty, + context.viewState.leaveHandle.canLeave { LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) { Section { - ForEach(leaveHandle.rooms, id: \.spaceRoomProxy.id) { room in + ForEach(context.viewState.leaveHandle.rooms, id: \.spaceRoomProxy.id) { room in LeaveSpaceRoomDetailsCell(room: room, - hideSelection: leaveHandle.mode == .onlyAdminRooms, + hideSelection: context.viewState.leaveHandle.mode == .onlyAdminRooms, mediaProvider: context.mediaProvider) { - context.send(viewAction: .toggleLeaveSpaceRoomDetails(id: room.spaceRoomProxy.id)) + context.send(viewAction: .toggleRoom(roomID: room.spaceRoomProxy.id)) } .disabled(room.isLastAdmin) } } header: { - if leaveHandle.mode == .manyRooms { - Button(leaveHandle.selectedCount > 0 ? L10n.actionDeselectAll : L10n.actionSelectAll) { - context.send(viewAction: leaveHandle.selectedCount > 0 ? .deselectAllLeaveRoomDetails : .selectAllLeaveRoomDetails) + if context.viewState.leaveHandle.mode == .manyRooms { + Button(context.viewState.leaveHandle.selectedCount > 0 ? L10n.actionDeselectAll : L10n.actionSelectAll) { + context.send(viewAction: context.viewState.leaveHandle.selectedCount > 0 ? .deselectAll : .selectAll) } .buttonStyle(.compound(.textLink, size: .small)) .frame(maxWidth: .infinity, alignment: .trailing) @@ -90,11 +90,11 @@ struct LeaveSpaceView: View { var buttons: some View { VStack(spacing: 16) { - if leaveHandle.canLeave { + if context.viewState.leaveHandle.canLeave { Button(role: .destructive) { context.send(viewAction: .confirmLeaveSpace) } label: { - Label(leaveHandle.confirmationTitle, icon: \.leave) + Label(context.viewState.confirmationTitle, icon: \.leave) } .buttonStyle(.compound(.primary)) } else if context.viewState.canEditRolesAndPermissions { @@ -114,44 +114,24 @@ struct LeaveSpaceView: View { } } -private extension LeaveSpaceHandleProxy { - func title(spaceName: String) -> String { - switch mode { - case .lastSpaceAdmin: L10n.screenLeaveSpaceTitleLastAdmin(spaceName) - default: L10n.screenLeaveSpaceTitle(spaceName) - } - } - - var subtitle: String? { - switch mode { - case .manyRooms: L10n.screenLeaveSpaceSubtitle - case .onlyAdminRooms: L10n.screenLeaveSpaceSubtitleOnlyLastAdmin - case .noRooms: nil - case .lastSpaceAdmin: L10n.screenLeaveSpaceSubtitleLastAdmin - } - } - - var confirmationTitle: String { - let selectedCount = selectedCount - return selectedCount > 0 ? L10n.screenLeaveSpaceSubmit(selectedCount) : L10n.actionLeaveSpace - } -} - // MARK: - Previews import MatrixRustSDK struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { - static let viewModel = makeViewModel() + static let manyViewModel = makeViewModel(mode: .manyRooms) + static let onlyAdminViewModel = makeViewModel(mode: .onlyAdminRooms) + static let noRoomsViewModel = makeViewModel(mode: .noRooms) + static let lastAdminViewModel = makeViewModel(mode: .lastSpaceAdmin) static var previews: some View { - LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .manyRooms)) + LeaveSpaceView(context: manyViewModel.context) .previewDisplayName("Many Rooms") - LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .onlyAdminRooms)) + LeaveSpaceView(context: onlyAdminViewModel.context) .previewDisplayName("Only Admin Rooms") - LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .noRooms)) + LeaveSpaceView(context: noRoomsViewModel.context) .previewDisplayName("No Rooms") - LeaveSpaceView(context: viewModel.context, leaveHandle: makeLeaveHandle(mode: .lastSpaceAdmin)) + LeaveSpaceView(context: lastAdminViewModel.context) .previewDisplayName("Last Space Admin") } @@ -164,21 +144,7 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { 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 spaceServiceProxy = SpaceServiceProxyMock(.init()) - - let viewModel = SpaceScreenViewModel(spaceRoomListProxy: spaceRoomListProxy, - spaceServiceProxy: spaceServiceProxy, - selectedSpaceRoomPublisher: .init(nil), - userSession: UserSessionMock(.init()), - appSettings: AppSettings(), - userIndicatorController: UserIndicatorControllerMock()) - return viewModel - } - - static func makeLeaveHandle(mode: LeaveSpaceHandleProxy.Mode) -> LeaveSpaceHandleProxy { + static func makeViewModel(mode: LeaveSpaceHandleProxy.Mode) -> LeaveSpaceViewModel { let rooms: [LeaveSpaceRoom] = switch mode { case .manyRooms: .mockRooms case .onlyAdminRooms: .mockAdminRooms @@ -186,7 +152,13 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { case .lastSpaceAdmin: .mockLastSpaceAdmin(spaceRoomProxy: spaceRoomProxy) } - return LeaveSpaceHandleProxy(spaceID: spaceRoomProxy.id, - leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: rooms))) + let leaveHandle = LeaveSpaceHandleProxy(spaceID: spaceRoomProxy.id, + leaveHandle: LeaveSpaceHandleSDKMock(.init(rooms: rooms))) + + return LeaveSpaceViewModel(spaceName: spaceRoomProxy.name, + canEditRolesAndPermissions: true, + leaveHandle: leaveHandle, + userIndicatorController: UserIndicatorControllerMock(), + mediaProvider: MediaProviderMock(configuration: .init())) } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index dc3f62c81..63db22780 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -27,6 +27,7 @@ enum SpaceScreenCoordinatorAction { case leftSpace case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) + case displayRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) } final class SpaceScreenCoordinator: CoordinatorProtocol { @@ -69,6 +70,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.displayMembers(roomProxy: roomProxy)) case .displaySpaceSettings(let roomProxy): actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy)) + case .presentRolesAndPermissions(let roomProxy): + actionsSubject.send(.displayRolesAndPermissions(roomProxy: roomProxy)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index ff19b7a0d..13c91bdd4 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -13,6 +13,7 @@ enum SpaceScreenViewModelAction { case selectUnjoinedSpace(SpaceRoomProxyProtocol) case selectRoom(roomID: String) case leftSpace + case presentRolesAndPermissions(roomProxy: JoinedRoomProxyProtocol) case displayMembers(roomProxy: JoinedRoomProxyProtocol) case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) } @@ -39,17 +40,12 @@ struct SpaceScreenViewState: BindableState { } struct SpaceScreenViewStateBindings { - var leaveHandle: LeaveSpaceHandleProxy? + var leaveSpaceViewModel: LeaveSpaceViewModel? } enum SpaceScreenViewAction { case spaceAction(SpaceRoomCell.Action) case leaveSpace - case deselectAllLeaveRoomDetails - case selectAllLeaveRoomDetails - case toggleLeaveSpaceRoomDetails(id: String) - case confirmLeaveSpace case spaceSettings(roomProxy: JoinedRoomProxyProtocol) - 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 bbcb62ca7..5f25dad03 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -15,6 +15,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc private let spaceRoomListProxy: SpaceRoomListProxyProtocol private let spaceServiceProxy: SpaceServiceProxyProtocol private let clientProxy: ClientProxyProtocol + private let mediaProvider: MediaProviderProtocol private let appSettings: AppSettings private let userIndicatorController: UserIndicatorControllerProtocol @@ -32,6 +33,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc self.spaceRoomListProxy = spaceRoomListProxy self.spaceServiceProxy = spaceServiceProxy clientProxy = userSession.clientProxy + mediaProvider = userSession.mediaProvider self.userIndicatorController = userIndicatorController self.appSettings = appSettings @@ -117,31 +119,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc Task { await join(spaceRoomProxy) } case .leaveSpace: Task { await showLeaveSpaceConfirmation() } - case .deselectAllLeaveRoomDetails: - guard let leaveHandle = state.bindings.leaveHandle else { fatalError("The leave handle should be available.") } - for room in leaveHandle.rooms { - room.isSelected = false - } - case .selectAllLeaveRoomDetails: - guard let leaveHandle = state.bindings.leaveHandle else { fatalError("The leave handle should be available.") } - for room in leaveHandle.rooms where !room.isLastAdmin { - room.isSelected = true - } - case .toggleLeaveSpaceRoomDetails(let spaceRoomID): - guard let room = state.bindings.leaveHandle?.rooms.first(where: { $0.spaceRoomProxy.id == spaceRoomID }) else { - fatalError("The space room to toggle is not in the list of rooms to leave.") - } - withTransaction(\.disablesAnimations, true) { // The button is adding an unwanted animation. - room.isSelected.toggle() - } - case .confirmLeaveSpace: - Task { await confirmLeaveSpace() } case .displayMembers(let roomProxy): actionsSubject.send(.displayMembers(roomProxy: roomProxy)) case .spaceSettings(let roomProxy): actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy)) - case .rolesAndPermissions: - break // Not implemented yet } } @@ -180,41 +161,37 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc return } - state.bindings.leaveHandle = leaveHandle - } - - private func confirmLeaveSpace() async { - guard let leaveHandle = state.bindings.leaveHandle else { fatalError("Leaving without a handle is impossible.") } - - showLeavingIndicator() - defer { hideLeavingIndicator() } - - switch await leaveHandle.leave() { - case .success: - state.bindings.leaveHandle = nil - actionsSubject.send(.leftSpace) - case .failure: - showFailureIndicator() + let leaveSpaceViewModel = LeaveSpaceViewModel(spaceName: state.space.name, + canEditRolesAndPermissions: appSettings.spaceSettingsEnabled && state.canEditRolesAndPermissions, + leaveHandle: leaveHandle, + userIndicatorController: userIndicatorController, + mediaProvider: mediaProvider) + leaveSpaceViewModel.actions.sink { [weak self] action in + guard let self else { return } + switch action { + case .didCancel: + state.bindings.leaveSpaceViewModel = nil + case .presentRolesAndPermissions: + guard let roomProxy = state.roomProxy else { + fatalError("The space screen should always have a room proxy") + } + state.bindings.leaveSpaceViewModel = nil + actionsSubject.send(.presentRolesAndPermissions(roomProxy: roomProxy)) + case .didLeaveSpace: + state.bindings.leaveSpaceViewModel = nil + actionsSubject.send(.leftSpace) + } } + .store(in: &cancellables) + + state.bindings.leaveSpaceViewModel = leaveSpaceViewModel } - - private func updatePermissions() { } - + // MARK: - Indicators private static var leavingIndicatorID: String { "\(Self.self)-Leaving" } private static var failureIndicatorID: String { "\(Self.self)-Failure" } - private func showLeavingIndicator() { - userIndicatorController.submitIndicator(UserIndicator(id: Self.leavingIndicatorID, - type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), - title: L10n.commonLeavingSpace)) - } - - private func hideLeavingIndicator() { - userIndicatorController.retractIndicatorWithId(Self.leavingIndicatorID) - } - private func showFailureIndicator() { userIndicatorController.submitIndicator(UserIndicator(id: Self.failureIndicatorID, type: .toast, diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index ccea15ba9..04ec4bafa 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -25,8 +25,8 @@ struct SpaceScreen: View { .navigationTitle(context.viewState.space.name) .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } - .sheet(item: $context.leaveHandle) { leaveHandle in - LeaveSpaceView(context: context, leaveHandle: leaveHandle) + .sheet(item: $context.leaveSpaceViewModel) { leaveSpaceViewModel in + LeaveSpaceView(context: leaveSpaceViewModel.context) } } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift deleted file mode 100644 index 3b42d693b..000000000 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// 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 { - let roomProxy: JoinedRoomProxyProtocol - let userSession: UserSessionProtocol - let analyticsService: AnalyticsService - let userIndicator: UserIndicatorControllerProtocol - let notificationSettingsProxy: NotificationSettingsProxyProtocol - let attributedStringBuilder: AttributedStringBuilderProtocol - let appSettings: AppSettings -} - -enum SpaceSettingsScreenCoordinatorAction { - case presentEditDetailsScreen - case presentSecurityAndPrivacyScreen - case presentMembersListScreen - case presentRolesAndPermissionsScreen -} - -final class SpaceSettingsScreenCoordinator: CoordinatorProtocol { - private let viewModel: RoomDetailsScreenViewModelProtocol - - private var cancellables = Set() - - private let actionsSubject: PassthroughSubject = .init() - var actionsPublisher: AnyPublisher { - actionsSubject.eraseToAnyPublisher() - } - - init(parameters: SpaceSettingsScreenCoordinatorParameters) { - viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy, - userSession: parameters.userSession, - analyticsService: parameters.analyticsService, - userIndicatorController: parameters.userIndicator, - notificationSettingsProxy: parameters.notificationSettingsProxy, - attributedStringBuilder: parameters.attributedStringBuilder, - appSettings: parameters.appSettings) - } - - func start() { - viewModel.actions.sink { [weak self] action in - MXLog.info("Coordinator: received view model action: \(action)") - guard let self else { return } - - switch action { - case .requestNotificationSettingsPresentation, .requestRecipientDetailsPresentation, .requestInvitePeoplePresentation, .requestPollsHistoryPresentation, - .startCall, .displayPinnedEventsTimeline, .displayMediaEventsTimeline, .displayKnockingRequests, - .displayReportRoom, .transferOwnership: - break // Not handled in this context - case .requestEditDetailsPresentation: - actionsSubject.send(.presentEditDetailsScreen) - case .displaySecurityAndPrivacy: - actionsSubject.send(.presentSecurityAndPrivacyScreen) - case .requestMemberDetailsPresentation: - actionsSubject.send(.presentMembersListScreen) - case .requestRolesAndPermissionsPresentation: - actionsSubject.send(.presentRolesAndPermissionsScreen) - case .leftRoom: - break // TODO: - } - } - .store(in: &cancellables) - } - - func toPresentable() -> AnyView { - AnyView(SpaceSettingsScreen(context: viewModel.context)) - } -} diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift index 3a4c17daf..3bf183f2f 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift @@ -22,6 +22,9 @@ struct SpaceSettingsScreen: View { } .compoundList() .navigationTitle(L10n.commonSettings) + .sheet(item: $context.leaveSpaceViewModel) { viewModel in + LeaveSpaceView(context: viewModel.context) + } } private var editSection: some View { diff --git a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift index a3a08bdb5..93b54330a 100644 --- a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift +++ b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift @@ -9,7 +9,7 @@ import Foundation import MatrixRustSDK -class LeaveSpaceHandleProxy: Identifiable { +final class LeaveSpaceHandleProxy { let id: String var rooms: [LeaveSpaceRoomDetails] @@ -51,6 +51,25 @@ class LeaveSpaceHandleProxy: Identifiable { } } + func deselectAll() { + for room in rooms { + room.isSelected = false + } + } + + func selectAll() { + for room in rooms where !room.isLastAdmin { + room.isSelected = true + } + } + + func toggleRoom(roomID: String) { + guard let room = rooms.first(where: { $0.spaceRoomProxy.id == roomID }) else { + return + } + room.isSelected.toggle() + } + func leave() async -> Result { let selectedRoomIDs = rooms.filter(\.isSelected).map(\.spaceRoomProxy.id) 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 index ee3e2c638..2e6ef5955 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c396362920b57958f7e0586fe9161ad7808e91c62e9457c6dbb0379c04b1b488 -size 92482 +oid sha256:b2fe3c4f6354f94523ee75f69ed37b702aac943a095bcee679a6743be420a2a2 +size 103431 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 index 0b3f707bd..ab6efc2b9 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/leaveSpaceView.Last-Space-Admin-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3316fbd3a5c7b5310f940bef50381a3a947bfe82c836fe212f3a12d301ccf439 -size 100701 +oid sha256:b6087fcb56a01cebe9cbe17ce6e87764a3b586fdb4443b8a5a6c7be9fb5bbc7d +size 113376 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 index 565c10c9f..bb3166ddc 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4626895535cc88986dc35a8ddb0fac304ca3cc1bf21db3a58daa6efb66912132 -size 52109 +oid sha256:f8d1d8999c2df94cc24c7b11a3727878ce3dd7bfde23910da358fe5293fd58f7 +size 62111 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 index b0b234d2e..e87a913e2 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef1fa43dc064fc2426bfa091eafcf4234f3a85ba2631dd95d5b0dca2648ddac5 -size 70188 +oid sha256:7d3418e7499a070280e0305e06b4b3a318d9acb73befe72cd9c42efa88451828 +size 86087 diff --git a/UnitTests/Sources/SpaceScreenViewModelTests.swift b/UnitTests/Sources/SpaceScreenViewModelTests.swift index 1d343e501..5594021aa 100644 --- a/UnitTests/Sources/SpaceScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceScreenViewModelTests.swift @@ -178,29 +178,30 @@ class SpaceScreenViewModelTests: XCTestCase { func testLeavingSpace() async throws { setupViewModel() - XCTAssertNil(context.leaveHandle) + XCTAssertNil(context.leaveSpaceViewModel) - let deferredHandle = deferFulfillment(context.observe(\.leaveHandle)) { $0 != nil } + let deferredHandle = deferFulfillment(context.observe(\.leaveSpaceViewModel)) { $0 != nil } context.send(viewAction: .leaveSpace) try await deferredHandle.fulfill() - XCTAssertNotNil(context.leaveHandle, "The leave action should show the leave view.") + XCTAssertNotNil(context.leaveSpaceViewModel, "The leave action should show the leave view.") - let handle = try XCTUnwrap(context.leaveHandle) + let leaveSpaceViewModel = try XCTUnwrap(context.leaveSpaceViewModel) + let handle = try XCTUnwrap(context.leaveSpaceViewModel?.state.leaveHandle) let selectedCount = handle.selectedCount let firstSelectedRoom = try XCTUnwrap(handle.rooms.first { $0.isSelected }) XCTAssertGreaterThan(selectedCount, 0, "The leave view should have selected rooms to begin with") - context.send(viewAction: .deselectAllLeaveRoomDetails) + leaveSpaceViewModel.context.send(viewAction: .deselectAll) XCTAssertEqual(handle.selectedCount, 0, "Deselecting all should result in no selected rooms.") - context.send(viewAction: .toggleLeaveSpaceRoomDetails(id: firstSelectedRoom.spaceRoomProxy.id)) + leaveSpaceViewModel.context.send(viewAction: .toggleRoom(roomID: firstSelectedRoom.spaceRoomProxy.id)) XCTAssertEqual(handle.selectedCount, 1, "Toggling a room should result in 1 selected room") // Confirming the leave should leave the selected room and then the space. let deferredAction = deferFulfillment(viewModel.actionsPublisher) { $0.isLeftSpace } - context.send(viewAction: .confirmLeaveSpace) + leaveSpaceViewModel.context.send(viewAction: .confirmLeaveSpace) try await deferredAction.fulfill() - XCTAssertNil(context.leaveHandle) + XCTAssertNil(context.leaveSpaceViewModel) XCTAssertTrue(rustLeaveHandle.leaveRoomIdsCalled) XCTAssertEqual(rustLeaveHandle.leaveRoomIdsReceivedRoomIds, [firstSelectedRoom.spaceRoomProxy.id, spaceRoomListProxy.id],