From 943e5506580f90558c7fdd81679f82d0e24afa09 Mon Sep 17 00:00:00 2001 From: Mauro Romito Date: Tue, 28 Oct 2025 18:40:52 +0100 Subject: [PATCH] implemented the basics of the flow coordinator, the logic and the navigation flow to get to the space settings view --- .../Sources/GeneratedAccessibilityTests.swift | 4 + ElementX.xcodeproj/project.pbxproj | 40 +++++++ .../Application/Settings/AppSettings.swift | 4 + .../SpaceFlowCoordinator.swift | 50 ++++++++- .../SpaceSettingsFlowCoordinator.swift | 101 ++++++++++++++++++ .../TestablePreviewsDictionary.swift | 1 + .../DeveloperOptionsScreenModels.swift | 2 + .../View/DeveloperOptionsScreen.swift | 6 ++ .../SpaceScreen/SpaceScreenCoordinator.swift | 5 + .../SpaceScreen/SpaceScreenModels.swift | 1 + .../SpaceScreen/SpaceScreenViewModel.swift | 30 +++++- .../SpaceScreen/View/LeaveSpaceView.swift | 1 + .../Spaces/SpaceScreen/View/SpaceScreen.swift | 7 ++ .../SpaceSettingsScreenCoordinator.swift | 45 ++++++++ .../SpaceSettingsScreenModels.swift | 32 ++++++ .../SpaceSettingsScreenViewModel.swift | 47 ++++++++ ...SpaceSettingsScreenViewModelProtocol.swift | 14 +++ .../View/SpaceSettingsScreen.swift | 71 ++++++++++++ .../Sources/GeneratedPreviewTests.swift | 6 ++ 19 files changed, 460 insertions(+), 7 deletions(-) create mode 100644 ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift create mode 100644 ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift create mode 100644 ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift create mode 100644 ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift create mode 100644 ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModelProtocol.swift create mode 100644 ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift diff --git a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift index babbe67f3..66883ca2a 100644 --- a/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift +++ b/AccessibilityTests/Sources/GeneratedAccessibilityTests.swift @@ -631,6 +631,10 @@ extension AccessibilityTests { try await performAccessibilityAudit(named: "SpaceScreen_Previews") } + func testSpaceSettingsScreen() async throws { + try await performAccessibilityAudit(named: "SpaceSettingsScreen_Previews") + } + func testSpacesAnnouncementSheetView() async throws { try await performAccessibilityAudit(named: "SpacesAnnouncementSheetView_Previews") } diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index bc8134163..1cea7a429 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -174,6 +174,7 @@ 1C1750C009F7214B967928BC /* ManageRoomMemberSheetViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D80807B554CF9C524F98674F /* ManageRoomMemberSheetViewModelTests.swift */; }; 1C409A26A99F0371C47AFA51 /* UserDiscoveryServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F615A00DB223FF3280204D2 /* UserDiscoveryServiceProtocol.swift */; }; 1C4CB9009E50E6535883D5B2 /* RestorationToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3558A15CFB934F9229301527 /* RestorationToken.swift */; }; + 1C5615383D04E64E5AF9271E /* SpaceSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A0EDCE8F0C1AC85840CCC6A /* SpaceSettingsScreenViewModel.swift */; }; 1C598D3B785645AAC7B35760 /* ReportRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292EEE1F71DCC205C45728F7 /* ReportRoomScreenCoordinator.swift */; }; 1C6B06DB15EC194AF35C05DB /* RoomPowerLevelsProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */; }; 1C8BC70A18060677E295A846 /* ShareToMapsAppActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4481799F455B3DA243BDA2AC /* ShareToMapsAppActivity.swift */; }; @@ -316,6 +317,7 @@ 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; }; 37E47F5101C0C036289D3807 /* SwiftOGG in Frameworks */ = {isa = PBXBuildFile; productRef = 391D11F92DFC91666AA1503F /* SwiftOGG */; }; 37EE1FB8400BBDC7A7338E57 /* LeaveSpaceRoomDetailsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B329F7962435DB1B5F49F2AC /* LeaveSpaceRoomDetailsCell.swift */; }; + 383063A7924F06D54BA9B24C /* SpaceSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9475FD81B13D50103E5290EB /* SpaceSettingsScreen.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 */; }; @@ -1082,6 +1084,7 @@ C3317EF833AB4060988DF098 /* SAS.strings in Resources */ = {isa = PBXBuildFile; fileRef = 135FC689EA39AE1D34153B58 /* SAS.strings */; }; C3522917C0C367C403429EEC /* CoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B251F5B4511D1CA0BA8361FE /* CoordinatorProtocol.swift */; }; C3AFDF6349E54290AA31EC88 /* preview_video.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 45A4B934BA41D6C255900265 /* preview_video.jpg */; }; + C3BB48F26EAFE9DF00ECBC44 /* SpaceSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27CD1C224961E86C6732734 /* SpaceSettingsScreenModels.swift */; }; C3BB6887CF13B19182E81F87 /* IdentityConfirmationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A03E073077D92AA19C43DCF /* IdentityConfirmationScreenCoordinator.swift */; }; C405528EB4BBEA93579050EE /* VoiceMessageRecordingButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46A2AD86F7E618F468F6FAF5 /* VoiceMessageRecordingButton.swift */; }; C4078364FD9FA00EA9D00A15 /* RoomMembersListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */; }; @@ -1156,6 +1159,7 @@ D02DEB36D32A72A1B365E452 /* SessionVerificationScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 796CBD0C56FA0D3AEDAB255B /* SessionVerificationScreenCoordinator.swift */; }; D050D7756E92CA061ED0ABF0 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74E08B8A66948E9690F38B94 /* SecureBackupLogoutConfirmationScreenViewModelProtocol.swift */; }; D0A965852D6C04138FA55181 /* SecureBackupLogoutConfirmationScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */; }; + D0E257557DAC8A34C7B52A9F /* SpaceSettingsFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDE3EDEA7E64D68FEB828F83 /* SpaceSettingsFlowCoordinator.swift */; }; D104B27C5DA0626B41CE78D3 /* CurrentValuePublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */; }; D10BA4F041DC58580A440A32 /* RoomRolesAndPermissionsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B1DC3B3FB40A7F4AE9B7BF /* RoomRolesAndPermissionsScreen.swift */; }; D12F440F7973F1489F61389D /* NotificationSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F64447FF544298A6A3BEF85 /* NotificationSettingsScreenModels.swift */; }; @@ -1318,6 +1322,7 @@ F08F7BC07CA9AEF5CD157918 /* Snapshotting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF17EA323AD0205A6AB621AA /* Snapshotting.swift */; }; F0A027BB2369606DBDE3BDAD /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 32B8F4CD937AA9C1F8FC3CBC /* KeychainAccess */; }; F0A26CD502C3A5868353B0FA /* ServerConfirmationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24DEE0682C95F897B6C7CB0D /* ServerConfirmationScreenViewModel.swift */; }; + F0D3973B02657D6F905B03B7 /* SpaceSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 703108B1EFD8CD64BED40A35 /* SpaceSettingsScreenViewModelProtocol.swift */; }; F0DACC95F24128A54CD537E4 /* GlobalSearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24B8177BD2AF45A286F5DA31 /* GlobalSearchScreen.swift */; }; F0F82C3C848C865C3098AA52 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = BA93CD75CCE486660C9040BD /* Collections */; }; F103924DED414ADFE398CE99 /* RoomPollsHistoryScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */; }; @@ -1395,6 +1400,7 @@ 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 */; }; @@ -2075,6 +2081,7 @@ 6FA38E813BE14149F173F461 /* PinnedEventsBannerStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedEventsBannerStateTests.swift; sourceTree = ""; }; 6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; }; 6FC8B21E86B137BE4E91F82A /* ElementCallServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallServiceProtocol.swift; sourceTree = ""; }; + 703108B1EFD8CD64BED40A35 /* SpaceSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; 7033218DA395B003F7AB29A2 /* MediaEventsTimelineScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaEventsTimelineScreenModels.swift; sourceTree = ""; }; 7061BE2C0BF427C38AEDEF5E /* SecureBackupRecoveryKeyScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModel.swift; sourceTree = ""; }; 70C86696AC9521F8ED88FBEB /* MediaUploadPreviewScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreen.swift; sourceTree = ""; }; @@ -2216,6 +2223,7 @@ 89AAEA70CFF3284920811941 /* RoomChangePermissionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomChangePermissionsScreen.swift; sourceTree = ""; }; 89BB11A792EF6F70B95B467E /* EncryptionResetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetTests.swift; sourceTree = ""; }; 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormButtonStyles.swift; sourceTree = ""; }; + 8A0EDCE8F0C1AC85840CCC6A /* SpaceSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreenViewModel.swift; sourceTree = ""; }; 8A1F2AAA3F0F2B72D2FFE4D0 /* MapTilerConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTilerConfiguration.swift; sourceTree = ""; }; 8A9AE4967817E9608E22EB44 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 8AE0C9653870803E4F91F474 /* RoomListFiltersStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomListFiltersStateTests.swift; sourceTree = ""; }; @@ -2270,6 +2278,7 @@ 93E1FF0DFBB3768F79FDBF6D /* AVMetadataMachineReadableCodeObjectExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVMetadataMachineReadableCodeObjectExtensionsTest.swift; sourceTree = ""; }; 93E7304F5ECB4CB11CB10E60 /* SecureBackupRecoveryKeyScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupRecoveryKeyScreenViewModelProtocol.swift; sourceTree = ""; }; 94028A227645FA880B966211 /* WaveformSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaveformSource.swift; sourceTree = ""; }; + 9475FD81B13D50103E5290EB /* SpaceSettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreen.swift; sourceTree = ""; }; 94D670124FC3E84F23A62CCF /* APNSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APNSPayload.swift; sourceTree = ""; }; 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenModels.swift; sourceTree = ""; }; 951F277E0585E50AC91987C8 /* DeclineAndBlockScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -2487,6 +2496,7 @@ BC8AA23D4F37CC26564F63C5 /* LayoutMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutMocks.swift; sourceTree = ""; }; BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = ""; }; BD5480F03306234FC086E93B /* HomeScreenNewSoundBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenNewSoundBanner.swift; sourceTree = ""; }; + BDE3EDEA7E64D68FEB828F83 /* SpaceSettingsFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsFlowCoordinator.swift; sourceTree = ""; }; BE148A4FFEE853C5A281500C /* UNNotificationContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationContent.swift; sourceTree = ""; }; BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemReplyDetails.swift; sourceTree = ""; }; BE98688578F8B0541D853695 /* test_pdf.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test_pdf.pdf; sourceTree = ""; }; @@ -2678,6 +2688,7 @@ E20403084A320D588ACED200 /* ReportRoomScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportRoomScreenViewModelProtocol.swift; sourceTree = ""; }; E2520C4F33AA0C061D209C28 /* RoomMembersListScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenTests.swift; sourceTree = ""; }; E2776E63E02719B20758EB78 /* EditRoomAddressListRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressListRow.swift; sourceTree = ""; }; + E27CD1C224961E86C6732734 /* SpaceSettingsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpaceSettingsScreenModels.swift; sourceTree = ""; }; E2B1CC9AA154F4D5435BF60A /* Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comparable.swift; sourceTree = ""; }; E2F96CCBEAAA7F2185BFA354 /* ClientProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientProxyMock.swift; sourceTree = ""; }; E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModel.swift; sourceTree = ""; }; @@ -2749,6 +2760,7 @@ 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 = ""; }; @@ -4239,6 +4251,18 @@ path = BugReportScreen; sourceTree = ""; }; + 55312ACF4155CC5B2054AD75 /* SpaceSettingsScreen */ = { + isa = PBXGroup; + children = ( + EEA9DA018160CF76AFFBFBA7 /* SpaceSettingsScreenCoordinator.swift */, + E27CD1C224961E86C6732734 /* SpaceSettingsScreenModels.swift */, + 8A0EDCE8F0C1AC85840CCC6A /* SpaceSettingsScreenViewModel.swift */, + 703108B1EFD8CD64BED40A35 /* SpaceSettingsScreenViewModelProtocol.swift */, + 7C19E3A92E016D6E126DB06D /* View */, + ); + path = SpaceSettingsScreen; + sourceTree = ""; + }; 557C534BD2052BFFD810CE3D /* ShareExtension */ = { isa = PBXGroup; children = ( @@ -4280,6 +4304,7 @@ D28F7A6CEEA4A2815B0F0F55 /* SettingsFlowCoordinator.swift */, 5A4EF5724C0F894911AF7811 /* SpaceExplorerFlowCoordinator.swift */, EDDE826EAB1BAB80C1104980 /* SpaceFlowCoordinator.swift */, + BDE3EDEA7E64D68FEB828F83 /* SpaceSettingsFlowCoordinator.swift */, C99FDEEB71173C4C6FA2734C /* UserSessionFlowCoordinator.swift */, ); path = FlowCoordinators; @@ -4845,6 +4870,14 @@ path = NotificationSettingsScreen; sourceTree = ""; }; + 7C19E3A92E016D6E126DB06D /* View */ = { + isa = PBXGroup; + children = ( + 9475FD81B13D50103E5290EB /* SpaceSettingsScreen.swift */, + ); + path = View; + sourceTree = ""; + }; 7DA2A18CFD03E0BACE6B5C4B /* AnalyticsPromptScreen */ = { isa = PBXGroup; children = ( @@ -5292,6 +5325,7 @@ BDDD421CD80AD0BCBA035076 /* Common */, FCF165F4DDB83F3DECFEB57A /* SpaceListScreen */, C360FCF7418FE3593D5A0CBF /* SpaceScreen */, + 55312ACF4155CC5B2054AD75 /* SpaceSettingsScreen */, ); path = Spaces; sourceTree = ""; @@ -8303,6 +8337,12 @@ 94C2B531B96493B68B976E5F /* SpaceServiceProxy.swift in Sources */, A2091F4B1332D9BF273B09D5 /* SpaceServiceProxyMock.swift in Sources */, DB5200B87C4CE9DF0024AC4E /* SpaceServiceProxyProtocol.swift in Sources */, + D0E257557DAC8A34C7B52A9F /* SpaceSettingsFlowCoordinator.swift in Sources */, + 383063A7924F06D54BA9B24C /* SpaceSettingsScreen.swift in Sources */, + FD3C94F01ACAF2D4948CF9BE /* SpaceSettingsScreenCoordinator.swift in Sources */, + C3BB48F26EAFE9DF00ECBC44 /* SpaceSettingsScreenModels.swift in Sources */, + 1C5615383D04E64E5AF9271E /* SpaceSettingsScreenViewModel.swift in Sources */, + F0D3973B02657D6F905B03B7 /* SpaceSettingsScreenViewModelProtocol.swift in Sources */, 9DB4B303ECC05F0F33582594 /* SpacesAnnouncementSheetView.swift in Sources */, DF004A5B2EABBD0574D06A04 /* SplashScreenCoordinator.swift in Sources */, E1C67E5D9E22135A8FEBBD60 /* StackedAvatarsView.swift in Sources */, diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index e9b3bf146..2a6a24e22 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -65,6 +65,7 @@ final class AppSettings { case developerOptionsEnabled case linkPreviewsEnabled case latestEventSorterEnabled + case spaceSettingsEnabled // Doug's tweaks 🔧 case hideUnreadMessagesBadge @@ -388,6 +389,9 @@ final class AppSettings { @UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store)) var threadsEnabled + + @UserPreference(key: UserDefaultsKeys.spaceSettingsEnabled, defaultValue: false, storageType: .userDefaults(store)) + var spaceSettingsEnabled @UserPreference(key: UserDefaultsKeys.linkPreviewsEnabled, defaultValue: false, storageType: .userDefaults(store)) var linkPreviewsEnabled diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index 9d57b1a9a..861fbdd53 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -42,6 +42,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { private var childSpaceFlowCoordinator: SpaceFlowCoordinator? private var roomFlowCoordinator: RoomFlowCoordinator? private var membersFlowCoordinator: RoomMembersFlowCoordinator? + private var settingsFlowCoordinator: SpaceSettingsFlowCoordinator? indirect enum State: StateType { /// The state machine hasn't started. @@ -56,6 +57,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case roomFlow(previousState: State) /// A members flow is in progress case membersFlow + /// A space settings flow is in progress + case settingsFlow case leftSpace } @@ -83,6 +86,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case startMembersFlow case stopMembersFlow + + case startSettingsFlow + case stopSettingsFlow } private let stateMachine: StateMachine @@ -142,6 +148,9 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case .membersFlow: membersFlowCoordinator?.clearRoute(animated: animated) clearRoute(animated: animated) // Re-run with the state machine back in the .space state. + case .settingsFlow: + settingsFlowCoordinator?.clearRoute(animated: animated) + clearRoute(animated: animated) // Re-run with the state machine back in the .space state. } } @@ -212,7 +221,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { fatalError("The room proxy must always be provided") } - Task { await self.startMembersFlow(roomProxy: roomProxy) } + startMembersFlow(roomProxy: roomProxy) } stateMachine.addRouteMapping { event, fromState, _ in @@ -223,6 +232,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { membersFlowCoordinator = nil } + stateMachine.addRouteMapping { event, fromState, _ in + guard event == .startSettingsFlow, case .space = fromState else { return nil } + return .settingsFlow + } handler: { [weak self] context in + guard let self, let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } + startSettingsFlow(roomProxy: roomProxy) + } + + stateMachine.addRouteMapping { event, fromState, _ in + guard event == .stopSettingsFlow, case .settingsFlow = fromState else { return nil } + return .space + } handler: { [weak self] _ in + guard let self else { return } + settingsFlowCoordinator = nil + } + stateMachine.addErrorHandler { context in fatalError("Unexpected transition: \(context)") } @@ -235,6 +260,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { spaceServiceProxy: spaceServiceProxy, selectedSpaceRoomPublisher: selectedSpaceRoomSubject.asCurrentValuePublisher(), userSession: flowParameters.userSession, + appSettings: flowParameters.appSettings, userIndicatorController: flowParameters.userIndicatorController) let coordinator = SpaceScreenCoordinator(parameters: parameters) coordinator.actionsPublisher @@ -251,6 +277,8 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.leftSpace) case .displayMembers(let roomProxy): stateMachine.tryEvent(.startMembersFlow, userInfo: roomProxy) + case .displaySpaceSettings(roomProxy: let roomProxy): + stateMachine.tryEvent(.startSettingsFlow, userInfo: roomProxy) } } .store(in: &cancellables) @@ -372,7 +400,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { selectedSpaceRoomSubject.send(roomID) } - private func startMembersFlow(roomProxy: JoinedRoomProxyProtocol) async { + private func startMembersFlow(roomProxy: JoinedRoomProxyProtocol) { let flowCoordinator = RoomMembersFlowCoordinator(entryPoint: .roomMembersList, roomProxy: roomProxy, navigationStackCoordinator: navigationStackCoordinator, @@ -393,4 +421,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { membersFlowCoordinator = flowCoordinator flowCoordinator.start() } + + private func startSettingsFlow(roomProxy: JoinedRoomProxyProtocol) { + let flowCoordinator = SpaceSettingsFlowCoordinator(roomProxy: roomProxy, + navigationStackCoordinator: navigationStackCoordinator, + flowParameters: flowParameters) + + flowCoordinator.actions.sink { [weak self] actions in + guard let self else { return } + switch actions { + case .finished: + stateMachine.tryEvent(.stopSettingsFlow) + } + } + .store(in: &cancellables) + + settingsFlowCoordinator = flowCoordinator + flowCoordinator.start() + } } diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift new file mode 100644 index 000000000..6bb983924 --- /dev/null +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -0,0 +1,101 @@ +// +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import Foundation +import SwiftState + +enum SpaceSettingsFlowCoordinatorAction { + case finished +} + +final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { + indirect enum State: StateType { + /// The state machine hasn't started. + case initial + /// The space settings screen + case spaceSettings + } + + enum Event: EventType { + case start + + case presentSpaceSettings + } + + private let roomProxy: JoinedRoomProxyProtocol + private let navigationStackCoordinator: NavigationStackCoordinator + private let flowParameters: CommonFlowParameters + + private let stateMachine: StateMachine + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actions: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(roomProxy: JoinedRoomProxyProtocol, + navigationStackCoordinator: NavigationStackCoordinator, + flowParameters: CommonFlowParameters) { + self.roomProxy = roomProxy + self.flowParameters = flowParameters + self.navigationStackCoordinator = navigationStackCoordinator + + stateMachine = .init(state: .initial) + configureStateMachine() + } + + func start(animated: Bool) { + stateMachine.tryEvent(.presentSpaceSettings, userInfo: animated) + } + + func handleAppRoute(_ appRoute: AppRoute, animated: Bool) { + fatalError("Not implemented yet") + } + + func clearRoute(animated: Bool) { + // Not implemented yet + } + + private func configureStateMachine() { + stateMachine.addRouteMapping { event, fromState, _ in + switch (fromState, event) { + case (.initial, .presentSpaceSettings): + return .spaceSettings + + default: + return nil + } + } + + stateMachine.addAnyHandler(.any => .any) { [weak self] context in + guard let self else { return } + let animated = context.userInfo as? Bool ?? true + switch (context.fromState, context.event, context.toState) { + case (.initial, .presentSpaceSettings, .spaceSettings): + presentSpaceSettings(animated: animated) + + default: + fatalError("Unhandled transition") + } + } + } + + private func presentSpaceSettings(animated: Bool) { + let coordinator = SpaceSettingsScreenCoordinator(parameters: .init()) + + coordinator.actionsPublisher.sink { [weak self] action in + switch action { } + } + .store(in: &cancellables) + + navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in + self?.actionsSubject.send(.finished) + } + } +} diff --git a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift index 5ccf1c20a..89166e769 100644 --- a/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift +++ b/ElementX/Sources/Other/TestablePreview/TestablePreviewsDictionary.swift @@ -165,6 +165,7 @@ enum TestablePreviewsDictionary { "SpaceListScreen_Previews" : SpaceListScreen_Previews.self, "SpaceRoomCell_Previews" : SpaceRoomCell_Previews.self, "SpaceScreen_Previews" : SpaceScreen_Previews.self, + "SpaceSettingsScreen_Previews" : SpaceSettingsScreen_Previews.self, "SpacesAnnouncementSheetView_Previews" : SpacesAnnouncementSheetView_Previews.self, "SplashScreen_Previews" : SplashScreen_Previews.self, "StackedAvatarsView_Previews" : StackedAvatarsView_Previews.self, diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index bf4ce9bb3..83e249a8f 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -54,6 +54,8 @@ protocol DeveloperOptionsProtocol: AnyObject { var latestEventSorterEnabled: Bool { get set } var linkPreviewsEnabled: Bool { get set } + + var spaceSettingsEnabled: Bool { get set } } extension AppSettings: DeveloperOptionsProtocol { } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 5b6c9d9ed..08fcc2e48 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -33,6 +33,12 @@ struct DeveloperOptionsScreen: View { } } + Section("Spaces") { + Toggle(isOn: $context.spaceSettingsEnabled) { + Text("Space settings") + } + } + Section("Room List") { Toggle(isOn: $context.publicSearchEnabled) { Text("Public search") diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index faec25c81..dc3f62c81 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -16,6 +16,7 @@ struct SpaceScreenCoordinatorParameters { let spaceServiceProxy: SpaceServiceProxyProtocol let selectedSpaceRoomPublisher: CurrentValuePublisher let userSession: UserSessionProtocol + let appSettings: AppSettings let userIndicatorController: UserIndicatorControllerProtocol } @@ -25,6 +26,7 @@ enum SpaceScreenCoordinatorAction { case selectRoom(roomID: String) case leftSpace case displayMembers(roomProxy: JoinedRoomProxyProtocol) + case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) } final class SpaceScreenCoordinator: CoordinatorProtocol { @@ -45,6 +47,7 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { spaceServiceProxy: parameters.spaceServiceProxy, selectedSpaceRoomPublisher: parameters.selectedSpaceRoomPublisher, userSession: parameters.userSession, + appSettings: parameters.appSettings, userIndicatorController: parameters.userIndicatorController) } @@ -64,6 +67,8 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.leftSpace) case .displayMembers(let roomProxy): actionsSubject.send(.displayMembers(roomProxy: roomProxy)) + case .displaySpaceSettings(let roomProxy): + actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift index 5a4ab2e73..003528c41 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenModels.swift @@ -14,6 +14,7 @@ enum SpaceScreenViewModelAction { case selectRoom(roomID: String) case leftSpace case displayMembers(roomProxy: JoinedRoomProxyProtocol) + case displaySpaceSettings(roomProxy: JoinedRoomProxyProtocol) } struct SpaceScreenViewState: BindableState { diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index 4e879c7cc..a7444c06e 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 appSettings: AppSettings private let userIndicatorController: UserIndicatorControllerProtocol private let actionsSubject: PassthroughSubject = .init() @@ -26,11 +27,13 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc spaceServiceProxy: SpaceServiceProxyProtocol, selectedSpaceRoomPublisher: CurrentValuePublisher, userSession: UserSessionProtocol, + appSettings: AppSettings, userIndicatorController: UserIndicatorControllerProtocol) { self.spaceRoomListProxy = spaceRoomListProxy self.spaceServiceProxy = spaceServiceProxy clientProxy = userSession.clientProxy self.userIndicatorController = userIndicatorController + self.appSettings = appSettings super.init(initialViewState: SpaceScreenViewState(space: spaceRoomListProxy.spaceRoomProxyPublisher.value, rooms: spaceRoomListProxy.spaceRoomsPublisher.value, @@ -63,10 +66,6 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc } .store(in: &cancellables) - selectedSpaceRoomPublisher - .weakAssign(to: \.state.selectedSpaceRoomID, on: self) - .store(in: &cancellables) - Task { if case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(spaceRoomListProxy.id) { // Required to listen for membership updates in the members flow @@ -75,6 +74,22 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc if case let .success(permalinkURL) = await roomProxy.matrixToPermalink() { state.permalink = permalinkURL } + + appSettings.$spaceSettingsEnabled + .combineLatest(roomProxy.infoPublisher) + .sink { [weak self] isEnabled, info in + guard let self else { return } + guard isEnabled, let powerLevels = info.powerLevels else { + state.isSpaceManagementEnabled = false + return + } + + state.isSpaceManagementEnabled = powerLevels.canOwnUserEditRolesAndPermissions() || + powerLevels.canOwnUser(sendStateEvent: .roomName) || + powerLevels.canOwnUser(sendStateEvent: .roomTopic) || + powerLevels.canOwnUser(sendStateEvent: .roomAvatar) + } + .store(in: &cancellables) } } } @@ -122,7 +137,10 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc case .displayMembers(let roomProxy): actionsSubject.send(.displayMembers(roomProxy: roomProxy)) case .spaceSettings: - break // Not implemented. + guard let roomProxy = state.roomProxy else { + fatalError("Always available when the space settings button is tapped.") + } + actionsSubject.send(.displaySpaceSettings(roomProxy: roomProxy)) } } @@ -179,6 +197,8 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc } } + private func updatePermissions() { } + // MARK: - Indicators private static var leavingIndicatorID: String { "\(Self.self)-Leaving" } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift index 019af7be9..9e331d279 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/LeaveSpaceView.swift @@ -173,6 +173,7 @@ struct LeaveSpaceView_Previews: PreviewProvider, TestablePreview { spaceServiceProxy: spaceServiceProxy, selectedSpaceRoomPublisher: .init(nil), userSession: UserSessionMock(.init()), + appSettings: AppSettings(), userIndicatorController: UserIndicatorControllerMock()) return viewModel } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift index 0d74f01c6..6a43a3e5e 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/View/SpaceScreen.swift @@ -73,6 +73,12 @@ struct SpaceScreen: View { Label(L10n.actionShare, icon: \.shareIos) } } + + if context.viewState.isSpaceManagementEnabled { + Button { context.send(viewAction: .spaceSettings) } label: { + Label(L10n.commonSettings, icon: \.settings) + } + } } Section { @@ -121,6 +127,7 @@ struct SpaceScreen_Previews: PreviewProvider, TestablePreview { spaceServiceProxy: SpaceServiceProxyMock(.init()), selectedSpaceRoomPublisher: .init(nil), userSession: userSession, + appSettings: AppSettings(), userIndicatorController: UserIndicatorControllerMock()) return viewModel } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift new file mode 100644 index 000000000..749e62da1 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenCoordinator.swift @@ -0,0 +1,45 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import SwiftUI + +struct SpaceSettingsScreenCoordinatorParameters { } + +enum SpaceSettingsScreenCoordinatorAction { } + +final class SpaceSettingsScreenCoordinator: CoordinatorProtocol { + private let parameters: SpaceSettingsScreenCoordinatorParameters + private let viewModel: SpaceSettingsScreenViewModelProtocol + + private var cancellables = Set() + + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init(parameters: SpaceSettingsScreenCoordinatorParameters) { + self.parameters = parameters + + viewModel = SpaceSettingsScreenViewModel() + } + + func start() { + viewModel.actionsPublisher.sink { [weak self] action in + MXLog.info("Coordinator: received view model action: \(action)") + + guard let self else { return } + switch action { } + } + .store(in: &cancellables) + } + + func toPresentable() -> AnyView { + AnyView(SpaceSettingsScreen(context: viewModel.context)) + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift new file mode 100644 index 000000000..47779bd4f --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenModels.swift @@ -0,0 +1,32 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Foundation + +enum SpaceSettingsScreenViewModelAction { } + +struct SpaceSettingsScreenViewState: BindableState { + var title: String + var placeholder: String + var counter = 0 + + var bindings: SpaceSettingsScreenViewStateBindings +} + +struct SpaceSettingsScreenViewStateBindings { + var composerText: String +} + +enum SpaceSettingsScreenViewAction { + case done + case textChanged + + case incrementCounter + case decrementCounter + + // Consider adding CustomStringConvertible conformance if the actions contain PII +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift new file mode 100644 index 000000000..44e967f80 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModel.swift @@ -0,0 +1,47 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine +import SwiftUI + +typealias SpaceSettingsScreenViewModelType = StateStoreViewModelV2 + +class SpaceSettingsScreenViewModel: SpaceSettingsScreenViewModelType, SpaceSettingsScreenViewModelProtocol { + private let actionsSubject: PassthroughSubject = .init() + var actionsPublisher: AnyPublisher { + actionsSubject.eraseToAnyPublisher() + } + + init() { + super.init(initialViewState: SpaceSettingsScreenViewState(title: "SpaceSettings title", + placeholder: "Enter something here", + bindings: .init(composerText: "Initial composer text"))) + } + + // MARK: - Public + + override func process(viewAction: SpaceSettingsScreenViewAction) { + MXLog.info("View model: received view action: \(viewAction)") + + switch viewAction { + case .done: + break + case .textChanged: + MXLog.info("View model: composer text changed to: \(state.bindings.composerText)") + case .incrementCounter: + Task { + try await Task.sleep(for: .seconds(.random(in: 1.0...2.0))) + state.counter += 1 + } + case .decrementCounter: + Task { + try await Task.sleep(for: .seconds(.random(in: 1.0...2.0))) + state.counter -= 1 + } + } + } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModelProtocol.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModelProtocol.swift new file mode 100644 index 000000000..35e7a1b43 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/SpaceSettingsScreenViewModelProtocol.swift @@ -0,0 +1,14 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Combine + +@MainActor +protocol SpaceSettingsScreenViewModelProtocol { + var actionsPublisher: AnyPublisher { get } + var context: SpaceSettingsScreenViewModelType.Context { get } +} diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift new file mode 100644 index 000000000..22cddc1e3 --- /dev/null +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift @@ -0,0 +1,71 @@ +// +// Copyright 2025 Element Creations Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. +// Please see LICENSE files in the repository root for full details. +// + +import Compound +import SwiftUI + +struct SpaceSettingsScreen: View { + @Bindable var context: SpaceSettingsScreenViewModel.Context + + var body: some View { + Form { + Section { + ListRow(label: .plain(title: context.viewState.placeholder), + kind: .textField(text: $context.composerText)) + + ListRow(label: .centeredAction(title: L10n.actionDone, + icon: \.leave), + kind: .button { context.send(viewAction: .done) }) + } + + Section { + ListRow(label: .default(title: "Counter", icon: \.chart), + details: .counter(context.viewState.counter), + kind: .label) + ListRow(label: .default(title: "Increment", icon: \.plus), + kind: .button { context.send(viewAction: .incrementCounter) }) + ListRow(label: .default(title: "Decrement", icon: \.minus), + kind: .button { context.send(viewAction: .decrementCounter) }) + } + } + .compoundList() + .navigationTitle(context.viewState.title) + .onChange(of: context.composerText) { + context.send(viewAction: .textChanged) + } + } +} + +// MARK: - Previews + +struct SpaceSettingsScreen_Previews: PreviewProvider, TestablePreview { + static let viewModel = makeViewModel() + static let incrementedViewModel = makeViewModel(counterValue: 1) + + static var previews: some View { + NavigationStack { + SpaceSettingsScreen(context: viewModel.context) + } + .previewDisplayName("Initial") + + NavigationStack { + SpaceSettingsScreen(context: incrementedViewModel.context) + } + .previewDisplayName("Incremented") + .snapshotPreferences(expect: incrementedViewModel.context.observe(\.viewState.counter).map { $0 == 1 }.eraseToStream()) + } + + static func makeViewModel(counterValue: Int = 0) -> SpaceSettingsScreenViewModel { + let viewModel = SpaceSettingsScreenViewModel() + + for _ in 0..