From 979f526f197de0b04289d775ddcdb559d203ede7 Mon Sep 17 00:00:00 2001 From: Mauro <34335419+Velin92@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:40:14 +0200 Subject: [PATCH] Last Owner should edit admins, and not leave when is last (#4372) --- ElementX.xcodeproj/project.pbxproj | 63 ++++----- .../en.lproj/Localizable.strings | 9 +- .../en.lproj/Localizable.stringsdict | 4 +- .../it.lproj/Localizable.stringsdict | 4 +- .../Application/Navigation/AppRoutes.swift | 2 + .../ChatsFlowCoordinator.swift | 11 ++ .../EncryptionSettingsFlowCoordinator.swift | 2 +- .../RoomFlowCoordinator.swift | 41 ++++++ ...omRolesAndPermissionsFlowCoordinator.swift | 15 +- ElementX/Sources/Generated/Strings.swift | 10 ++ .../Mocks/Generated/GeneratedMocks.swift | 5 - .../Sources/Mocks/InvitedRoomProxyMock.swift | 2 +- .../Sources/Mocks/JoinedRoomProxyMock.swift | 2 +- .../Sources/Mocks/RoomMemberProxyMock.swift | 54 ++++++-- .../HomeScreen/HomeScreenCoordinator.swift | 3 + .../Screens/HomeScreen/HomeScreenModels.swift | 1 + .../HomeScreen/HomeScreenViewModel.swift | 36 ++++- .../RoomChangeRolesScreenCoordinator.swift | 2 +- .../RoomChangeRolesScreenModels.swift | 63 +++++++-- .../RoomChangeRolesScreenViewModel.swift | 51 ++++--- .../View/RoomChangeRolesScreen.swift | 52 +++++-- .../View/RoomChangeRolesScreenSection.swift | 30 ++-- .../RoomChangeRolesScreenSelectedItem.swift | 22 ++- .../RoomDetailsScreenCoordinator.swift | 3 + .../RoomDetailsScreenModels.swift | 3 + .../RoomDetailsScreenViewModel.swift | 49 +++++-- .../RoomRolesAndPermissionsScreenModels.swift | 6 +- ...omRolesAndPermissionsScreenViewModel.swift | 17 ++- .../View/RoomRolesAndPermissionsScreen.swift | 55 ++++++-- .../Services/Analytics/AnalyticsService.swift | 2 +- .../Helpers/RoomModerationRole.swift | 2 +- .../Room/RoomMember/RoomMemberDetails.swift | 57 +------- .../Room/RoomMember/RoomMemberProxy.swift | 2 - .../RoomMember/RoomMemberProxyProtocol.swift | 6 +- .../Services/Room/RoomPermissions.swift | 108 ++++----------- ElementX/Sources/Services/Room/RoomRole.swift | 129 ++++++++++++++++++ ...een.Administrator-or-Owners-iPad-en-GB.png | 3 + ...en.Administrator-or-Owners-iPad-pseudo.png | 3 + ...dministrator-or-Owners-iPhone-16-en-GB.png | 3 + ...ministrator-or-Owners-iPhone-16-pseudo.png | 3 + ...eRolesScreen.Administrators-iPad-en-GB.png | 4 +- ...RolesScreen.Administrators-iPad-pseudo.png | 4 +- ...sScreen.Administrators-iPhone-16-en-GB.png | 4 +- ...Screen.Administrators-iPhone-16-pseudo.png | 4 +- ...hangeRolesScreen.Moderators-iPad-en-GB.png | 4 +- ...angeRolesScreen.Moderators-iPad-pseudo.png | 4 +- ...RolesScreen.Moderators-iPhone-16-en-GB.png | 4 +- ...olesScreen.Moderators-iPhone-16-pseudo.png | 4 +- ...oomChangeRolesScreen.Owners-iPad-en-GB.png | 3 + ...omChangeRolesScreen.Owners-iPad-pseudo.png | 3 + ...angeRolesScreen.Owners-iPhone-16-en-GB.png | 3 + ...ngeRolesScreen.Owners-iPhone-16-pseudo.png | 3 + ...geRolesScreenSelectedItem.iPad-en-GB-0.png | 4 +- ...eRolesScreenSelectedItem.iPad-pseudo-0.png | 4 +- ...esScreenSelectedItem.iPhone-16-en-GB-0.png | 4 +- ...sScreenSelectedItem.iPhone-16-pseudo-0.png | 4 +- ...AndPermissionsScreen.Admin-iPad-en-GB.png} | 0 ...ndPermissionsScreen.Admin-iPad-pseudo.png} | 0 ...rmissionsScreen.Admin-iPhone-16-en-GB.png} | 0 ...missionsScreen.Admin-iPhone-16-pseudo.png} | 0 ...ndPermissionsScreen.Creator-iPad-en-GB.png | 3 + ...dPermissionsScreen.Creator-iPad-pseudo.png | 3 + ...missionsScreen.Creator-iPhone-16-en-GB.png | 3 + ...issionsScreen.Creator-iPhone-16-pseudo.png | 3 + UnitTests/Sources/LocalizationTests.swift | 19 +-- .../RoomChangeRolesScreenViewModelTests.swift | 24 ++-- ...esAndPermissionsScreenViewModelTests.swift | 4 +- 67 files changed, 691 insertions(+), 363 deletions(-) create mode 100644 ElementX/Sources/Services/Room/RoomRole.swift create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-pseudo.png rename PreviewTests/Sources/__Snapshots__/PreviewTests/{roomRolesAndPermissionsScreen.iPad-en-GB-0.png => roomRolesAndPermissionsScreen.Admin-iPad-en-GB.png} (100%) rename PreviewTests/Sources/__Snapshots__/PreviewTests/{roomRolesAndPermissionsScreen.iPad-pseudo-0.png => roomRolesAndPermissionsScreen.Admin-iPad-pseudo.png} (100%) rename PreviewTests/Sources/__Snapshots__/PreviewTests/{roomRolesAndPermissionsScreen.iPhone-16-en-GB-0.png => roomRolesAndPermissionsScreen.Admin-iPhone-16-en-GB.png} (100%) rename PreviewTests/Sources/__Snapshots__/PreviewTests/{roomRolesAndPermissionsScreen.iPhone-16-pseudo-0.png => roomRolesAndPermissionsScreen.Admin-iPhone-16-pseudo.png} (100%) create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-pseudo.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-en-GB.png create mode 100644 PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-pseudo.png diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8dedee471..627791e1c 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 */ @@ -905,6 +905,7 @@ A6FFC4C5154C446BAD6B40D8 /* TimelineItemProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8520AFD6680CBAD388F6D927 /* TimelineItemProvider.swift */; }; A722F426FD81FC67706BB1E0 /* CustomLayoutLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42236480CF0431535EBE8387 /* CustomLayoutLabelStyle.swift */; }; A74438ED16F8683A4B793E6A /* AnalyticsSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BCE3FAF40932AC7C7639AC4 /* AnalyticsSettingsScreenViewModel.swift */; }; + A7AF83892E4388BE00ECD64C /* RoomRole.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7AF83882E4388BB00ECD64C /* RoomRole.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A808DC3F72D15C6C5A52317E /* TimelineItemDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCDA016D05107DED3B9495CB /* TimelineItemDebugView.swift */; }; A816F7087C495D85048AC50E /* RoomMemberDetailsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B6E30BB748F3F480F077969 /* RoomMemberDetailsScreenModels.swift */; }; @@ -1459,7 +1460,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 = ""; }; @@ -1534,7 +1535,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 = ""; }; @@ -1642,7 +1643,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 = ""; }; @@ -1723,7 +1724,7 @@ 3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.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 = ""; }; 37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = ""; }; 37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = ""; }; @@ -2148,7 +2149,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 = ""; }; @@ -2270,6 +2271,7 @@ A768CA51A59B8A5D8C8FD599 /* AuthenticationStartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreen.swift; sourceTree = ""; }; A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = ""; }; A7A1B80FE6E3BA72F9C748AD /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = ""; }; + A7AF83882E4388BB00ECD64C /* RoomRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRole.swift; sourceTree = ""; }; 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 = ""; }; @@ -2290,7 +2292,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 = ""; }; @@ -2355,7 +2357,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 = ""; }; B68B31232312AFC844440BFE /* DeclineAndBlockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenModels.swift; sourceTree = ""; }; @@ -2383,7 +2385,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 = ""; }; @@ -2481,7 +2483,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 = ""; }; CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxy.swift; sourceTree = ""; }; D01FD1171FF40E34D707FD00 /* BigIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigIcon.swift; sourceTree = ""; }; D03D7ECAC68C2FFB8CF01BCB /* DeactivateAccountScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreen.swift; sourceTree = ""; }; @@ -2545,7 +2547,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 = ""; }; @@ -2591,7 +2593,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 = ""; }; @@ -2635,7 +2637,7 @@ 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 = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -3755,6 +3757,7 @@ 40E6246F03D1FE377BC5D963 /* Room */ = { isa = PBXGroup; children = ( + A7AF83882E4388BB00ECD64C /* RoomRole.swift */, C30F45308428A4D9FFDB2FB8 /* BannedRoomProxy.swift */, 0E95B3BDB80531C85CD50AE6 /* InvitedRoomProxy.swift */, 07C6B0B087FE6601C3F77816 /* JoinedRoomProxy.swift */, @@ -6699,7 +6702,6 @@ EC6D0C817B1C21D9D096505A /* XCRemoteSwiftPackageReference "Version" */, EE40B0E16A55BD23ECBFFD22 /* XCRemoteSwiftPackageReference "matrix-rich-text-editor-swift" */, ); - preferredProjectObjectVersion = 77; projectDirPath = ""; projectRoot = ""; targets = ( @@ -7899,6 +7901,7 @@ 617624A97BDBB75ED3DD8156 /* RoomScreenViewModelProtocol.swift in Sources */, 66832DE7B5C2E861045265DC /* RoomSelectionScreen.swift in Sources */, 18FDE4ED6D83B0771452B43D /* RoomSelectionScreenCoordinator.swift in Sources */, + A7AF83892E4388BE00ECD64C /* RoomRole.swift in Sources */, 27FEF0F40750465195C9D6D6 /* RoomSelectionScreenModels.swift in Sources */, 8DCA1F05C3BA6ED826F1599D /* RoomSelectionScreenViewModel.swift in Sources */, 39DFC4B9EB6A8757210BDEC6 /* RoomSelectionScreenViewModelProtocol.swift in Sources */, @@ -8446,9 +8449,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; @@ -8467,9 +8468,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; @@ -8491,9 +8490,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; @@ -8542,9 +8539,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)"; @@ -8570,9 +8565,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)"; @@ -8797,9 +8790,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; @@ -8818,9 +8809,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; @@ -8842,9 +8831,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/Resources/Localizations/en.lproj/Localizable.strings b/ElementX/Resources/Localizations/en.lproj/Localizable.strings index f67314ff2..5147ea802 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.strings +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.strings @@ -242,6 +242,10 @@ "common_poll_undisclosed_text" = "Results will show after the poll has ended"; "common_preparing" = "Preparing…"; "common_privacy_policy" = "Privacy policy"; +"common_private_room" = "Private room"; +"common_private_space" = "Private space"; +"common_public_room" = "Public room"; +"common_public_space" = "Public space"; "common_reaction" = "Reaction"; "common_reactions" = "Reactions"; "common_reason" = "Reason"; @@ -556,6 +560,7 @@ "screen_room_pinned_banner_view_all_button_title" = "View All"; "screen_room_single_knock_request_title" = "%1$@ wants to join this room"; "screen_room_single_knock_request_view_button_title" = "View"; +"screen_room_change_role_administrators_or_owners_title" = "Edit Admins or Owners"; "screen_room_details_pinned_events_row_title" = "Pinned messages"; "screen_room_details_profile_row_title" = "Profile"; "screen_room_details_requests_to_join_title" = "Requests to join"; @@ -714,7 +719,6 @@ "screen_create_room_action_create_room" = "New room"; "screen_create_room_error_creating_room" = "An error occurred when creating the room"; "screen_create_room_private_option_description" = "Only people invited can access this room. All messages are end-to-end encrypted."; -"screen_create_room_private_option_title" = "Private room"; "screen_create_room_public_option_description" = "Anyone can find this room.\nYou can change this anytime in room settings."; "screen_create_room_topic_label" = "Topic (optional)"; "screen_deactivate_account_confirmation_dialog_content" = "Please confirm that you want to deactivate your account. This action cannot be undone."; @@ -952,7 +956,6 @@ "screen_room_details_already_invited" = "Already invited"; "screen_room_details_badge_encrypted" = "Encrypted"; "screen_room_details_badge_not_encrypted" = "Not encrypted"; -"screen_room_details_badge_public" = "Public room"; "screen_room_details_edit_room_title" = "Edit Room"; "screen_room_details_edition_error" = "There was an unknown error and the information couldn't be changed."; "screen_room_details_edition_error_title" = "Unable to update room"; @@ -1295,6 +1298,7 @@ "screen_create_account_title" = "Create account"; "screen_create_poll_cancel_confirmation_content_ios" = "Your changes won’t be saved"; "screen_create_room_add_people_title" = "Invite people"; +"screen_create_room_private_option_title" = "Private room"; "screen_create_room_public_option_title" = "Public room"; "screen_create_room_room_name_label" = "Room name"; "screen_create_room_title" = "Create a room"; @@ -1324,6 +1328,7 @@ "screen_room_change_role_section_moderators" = "Moderators"; "screen_room_change_role_section_users" = "Members"; "screen_room_change_role_unsaved_changes_title" = "Save changes?"; +"screen_room_details_badge_public" = "Public room"; "screen_room_details_invite_people_title" = "Invite people"; "screen_room_details_leave_conversation_title" = "Leave conversation"; "screen_room_details_leave_room_title" = "Leave room"; diff --git a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict index 2491c9b70..6ce4b64dc 100644 --- a/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/en.lproj/Localizable.stringsdict @@ -77,9 +77,9 @@ NSStringFormatValueTypeKey d one - %1$d member + %1$d Member other - %1$d members + %1$d Members common_poll_votes_count diff --git a/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict b/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict index 1f66554e9..32b6b44f6 100644 --- a/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict +++ b/ElementX/Resources/Localizations/it.lproj/Localizable.stringsdict @@ -77,9 +77,9 @@ NSStringFormatValueTypeKey d one - %1$d membro + %1$d Membro other - %1$d membri + %1$d Membri common_poll_votes_count diff --git a/ElementX/Sources/Application/Navigation/AppRoutes.swift b/ElementX/Sources/Application/Navigation/AppRoutes.swift index 2d7408b68..4450f4c47 100644 --- a/ElementX/Sources/Application/Navigation/AppRoutes.swift +++ b/ElementX/Sources/Application/Navigation/AppRoutes.swift @@ -48,6 +48,8 @@ enum AppRoute: Hashable { case chatBackupSettings /// An external share request e.g. from the ShareExtension case share(ShareExtensionPayload) + /// The change roles screen of a room with the transfer ownership setting + case transferOwnership(roomID: String) /// Whether or not the route should be handled by the authentication flow. var isAuthenticationRoute: Bool { diff --git a/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift index b6c995356..9754aa437 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsFlowCoordinator.swift @@ -202,6 +202,12 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { } case .accountProvisioningLink: break // We always ignore this flow when logged in. + case .transferOwnership(let roomID): + if stateMachine.state.roomListSelectedRoomID == roomID { + roomFlowCoordinator?.handleAppRoute(appRoute, animated: animated) + } else { + stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .transferOwnership)) + } } } @@ -232,6 +238,7 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { case .roomDetails: .roomDetails(roomID: roomID) case .eventID(let eventID): .event(eventID: eventID, roomID: roomID, via: via) // ignored. case .share(let payload): .share(payload) + case .transferOwnership: .transferOwnership(roomID: roomID) } roomFlowCoordinator.handleAppRoute(route, animated: animated) } else { @@ -449,6 +456,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { actionsSubject.send(.logout) case .presentDeclineAndBlock(let userID, let roomID): stateMachine.processEvent(.presentDeclineAndBlockScreen(userID: userID, roomID: roomID)) + case .transferOwnership(let roomIdentifier): + handleAppRoute(.transferOwnership(roomID: roomIdentifier), animated: true) } } .store(in: &cancellables) @@ -555,6 +564,8 @@ class ChatsFlowCoordinator: FlowCoordinatorProtocol { coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: animated) case .share(let payload): coordinator.handleAppRoute(.share(payload), animated: animated) + case .transferOwnership: + coordinator.handleAppRoute(.transferOwnership(roomID: roomID), animated: animated) } Task { diff --git a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift index a7bc58c5c..0dc2b0a91 100644 --- a/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/EncryptionSettingsFlowCoordinator.swift @@ -82,7 +82,7 @@ class EncryptionSettingsFlowCoordinator: FlowCoordinatorProtocol { case .roomList, .room, .roomAlias, .childRoom, .childRoomAlias, .roomDetails, .roomMemberDetails, .userProfile, .event, .eventOnRoomAlias, .childEvent, .childEventOnRoomAlias, - .call, .genericCallLink, .settings, .share: + .call, .genericCallLink, .settings, .share, .transferOwnership: // These routes aren't in this flow so clear the entire stack. clearRoute(animated: animated) case .chatBackupSettings: diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index 66d6e9417..d636e797a 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -37,6 +37,8 @@ enum RoomFlowCoordinatorEntryPoint: Hashable { case roomDetails /// An external media share request case share(ShareExtensionPayload) + /// The flow to change the the owner of the room + case transferOwnership var isEventID: Bool { guard case .eventID = self else { return false } @@ -196,9 +198,44 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { break // These are converted to a room ID route one level above. case .accountProvisioningLink, .roomList, .userProfile, .call, .genericCallLink, .settings, .chatBackupSettings: break // These routes can't be handled. + case .transferOwnership(let roomID): + guard self.roomID == roomID else { fatalError("Navigation route doesn't belong to this room flow.") } + + Task { + if roomProxy == nil { + guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { + return + } + + await storeAndSubscribeToRoomProxy(roomProxy) + } + + presentTransferOwnershipScreen() + } } } + private func presentTransferOwnershipScreen() { + let parameters = RoomChangeRolesScreenCoordinatorParameters(mode: .owner, + roomProxy: roomProxy, + mediaProvider: userSession.mediaProvider, + userIndicatorController: userIndicatorController, + analytics: analytics) + let stackCoordinator = NavigationStackCoordinator() + let coordinator = RoomChangeRolesScreenCoordinator(parameters: parameters) + coordinator.actionsPublisher.sink { [weak self] action in + guard let self else { return } + switch action { + case .complete: + navigationStackCoordinator.setSheetCoordinator(nil) + } + } + .store(in: &cancellables) + + stackCoordinator.setRootCoordinator(coordinator) + navigationStackCoordinator.setSheetCoordinator(stackCoordinator, animated: true) + } + private func presentCallScreen(roomID: String) async { guard case let .joined(roomProxy) = await userSession.clientProxy.roomForIdentifier(roomID) else { return @@ -798,6 +835,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { stateMachine.tryEvent(.presentRoomMemberDetails(userID: userID)) case .presentReportRoomScreen: stateMachine.tryEvent(.presentReportRoomScreen) + case .transferOwnership: + presentTransferOwnershipScreen() } } .store(in: &cancellables) @@ -1555,6 +1594,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { coordinator.handleAppRoute(.roomDetails(roomID: roomID), animated: true) case .share(let payload): coordinator.handleAppRoute(.share(payload), animated: true) + case .transferOwnership: + coordinator.handleAppRoute(.transferOwnership(roomID: roomID), animated: true) } } diff --git a/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift index cbdd8f95d..3aad15409 100644 --- a/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomRolesAndPermissionsFlowCoordinator.swift @@ -107,7 +107,13 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { stateMachine.addRoutes(event: .changeRoles, transitions: [.rolesAndPermissionsScreen => .changingRoles]) { [weak self] context in guard let role = context.userInfo as? RoomRolesAndPermissionsScreenRole else { fatalError("Expected a role") } - self?.presentChangeRolesScreen(role: role) + let mode: RoomRole = switch role { + case .administrators: + .administrator + case .moderators: + .moderator + } + self?.presentChangeRolesScreen(mode: mode) } stateMachine.addRoutes(event: .finishedChangingRoles, transitions: [.changingRoles => .rolesAndPermissionsScreen]) @@ -150,12 +156,7 @@ class RoomRolesAndPermissionsFlowCoordinator: FlowCoordinatorProtocol { } } - private func presentChangeRolesScreen(role: RoomRolesAndPermissionsScreenRole) { - let mode = switch role { - case .administrators: RoomMemberDetails.Role.administrator - case .moderators: RoomMemberDetails.Role.moderator - } - + private func presentChangeRolesScreen(mode: RoomRole) { let parameters = RoomChangeRolesScreenCoordinatorParameters(mode: mode, roomProxy: roomProxy, mediaProvider: mediaProvider, diff --git a/ElementX/Sources/Generated/Strings.swift b/ElementX/Sources/Generated/Strings.swift index cf5083810..5578be42f 100644 --- a/ElementX/Sources/Generated/Strings.swift +++ b/ElementX/Sources/Generated/Strings.swift @@ -542,6 +542,14 @@ internal enum L10n { internal static var commonPreparing: String { return L10n.tr("Localizable", "common_preparing") } /// Privacy policy internal static var commonPrivacyPolicy: String { return L10n.tr("Localizable", "common_privacy_policy") } + /// Private room + internal static var commonPrivateRoom: String { return L10n.tr("Localizable", "common_private_room") } + /// Private space + internal static var commonPrivateSpace: String { return L10n.tr("Localizable", "common_private_space") } + /// Public room + internal static var commonPublicRoom: String { return L10n.tr("Localizable", "common_public_room") } + /// Public space + internal static var commonPublicSpace: String { return L10n.tr("Localizable", "common_public_space") } /// Reaction internal static var commonReaction: String { return L10n.tr("Localizable", "common_reaction") } /// Reactions @@ -2150,6 +2158,8 @@ internal enum L10n { internal static var screenRoomChangePermissionsRoomTopic: String { return L10n.tr("Localizable", "screen_room_change_permissions_room_topic") } /// Send messages internal static var screenRoomChangePermissionsSendMessages: String { return L10n.tr("Localizable", "screen_room_change_permissions_send_messages") } + /// Edit Admins or Owners + internal static var screenRoomChangeRoleAdministratorsOrOwnersTitle: String { return L10n.tr("Localizable", "screen_room_change_role_administrators_or_owners_title") } /// Edit Admins internal static var screenRoomChangeRoleAdministratorsTitle: String { return L10n.tr("Localizable", "screen_room_change_role_administrators_title") } /// You will not be able to undo this action. You are promoting the user to have the same power level as you. diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index 19b41ccbf..7f296995a 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -13334,11 +13334,6 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol, @unchecked Sendable { set(value) { underlyingPowerLevel = value } } var underlyingPowerLevel: RoomPowerLevel! - var role: RoomMemberRole { - get { return underlyingRole } - set(value) { underlyingRole = value } - } - var underlyingRole: RoomMemberRole! } class RoomMembershipDetailsProxyMock: RoomMembershipDetailsProxyProtocol, @unchecked Sendable { diff --git a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift index d34ce9214..ea5b2dd00 100644 --- a/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/InvitedRoomProxyMock.swift @@ -82,7 +82,7 @@ private extension RoomMember { powerLevel: proxy.powerLevel.rustPowerLevel, normalizedPowerLevel: proxy.powerLevel.rustPowerLevel, isIgnored: proxy.isIgnored, - suggestedRoleForPowerLevel: proxy.role, + suggestedRoleForPowerLevel: proxy.role.rustRole, membershipChangeReason: proxy.membershipChangeReason) } } diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index d99e01c9a..ad8d7361c 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -89,7 +89,7 @@ extension JoinedRoomProxyMock { guard case .success(let member) = await self?.getMember(userID: userID) else { return .failure(.sdkError(RoomProxyMockError.generic)) } - return .success(member.role) + return .success(member.role.rustRole) } updatePowerLevelsForUsersReturnValue = .success(()) diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index ac81dffd8..d81efd789 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -17,7 +17,6 @@ struct RoomMemberProxyMockConfiguration { var isIgnored = false var powerLevel = RoomPowerLevel(value: 0) - var role = RoomMemberRole.user } extension RoomMemberProxyMock { @@ -36,7 +35,6 @@ extension RoomMemberProxyMock { isIgnored = configuration.isIgnored powerLevel = configuration.powerLevel - role = configuration.role } // Mocks @@ -52,8 +50,15 @@ extension RoomMemberProxyMock { displayName: "Me", avatarURL: .mockMXCUserAvatar, membership: .join, - powerLevel: .init(value: 100), - role: .administrator)) + powerLevel: .init(value: 100))) + } + + static var mockMeCreator: RoomMemberProxyMock { + RoomMemberProxyMock(with: .init(userID: "@me:matrix.org", + displayName: "Me", + avatarURL: .mockMXCUserAvatar, + membership: .join, + powerLevel: .infinite)) } static var mockAlice: RoomMemberProxyMock { @@ -116,32 +121,28 @@ extension RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@admin:matrix.org", displayName: "Arthur", membership: .join, - powerLevel: .init(value: 100), - role: .administrator)) + powerLevel: .init(value: 100))) } static var mockCreator: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@creator:matrix.org", displayName: "God", membership: .join, - powerLevel: .infinite, - role: .creator)) + powerLevel: .infinite)) } static var mockOwner: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@owner:matrix.org", displayName: "Guinevere", membership: .join, - powerLevel: .value(150), - role: .administrator)) + powerLevel: .value(150))) } static var mockModerator: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@mod:matrix.org", displayName: "Merlin", membership: .join, - powerLevel: .init(value: 50), - role: .moderator)) + powerLevel: .init(value: 50))) } static var mockBanned: [RoomMemberProxyMock] { @@ -181,4 +182,33 @@ extension Array where Element == RoomMemberProxyMock { .mockAdmin, .mockModerator ] + + /// This also includes the creator and the owner role. + static let allMembersAsAdminV2: [RoomMemberProxyMock] = [ + .mockMeAdmin, + .mockAlice, + .mockBob, + .mockCharlie, + .mockDan, + .mockInvited, + .mockIgnored, + .mockAdmin, + .mockModerator, + .mockOwner, + .mockCreator + ] + + static let allMembersAsCreator: [RoomMemberProxyMock] = [ + .mockAdmin, + .mockAlice, + .mockBob, + .mockCharlie, + .mockDan, + .mockInvited, + .mockIgnored, + .mockModerator, + .mockCreator, + .mockMeCreator, + .mockOwner + ] } diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift index 4a7ac9bac..f163c88c0 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenCoordinator.swift @@ -24,6 +24,7 @@ enum HomeScreenCoordinatorAction { case presentReportRoom(roomIdentifier: String) case presentDeclineAndBlock(userID: String, roomID: String) case roomLeft(roomIdentifier: String) + case transferOwnership(roomIdentifier: String) case presentSettingsScreen case presentFeedbackScreen case presentSecureBackupSettings @@ -86,6 +87,8 @@ final class HomeScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.logout) case .presentDeclineAndBlock(let userID, let roomID): actionsSubject.send(.presentDeclineAndBlock(userID: userID, roomID: roomID)) + case .transferOwnership(let roomIdentifier): + actionsSubject.send(.transferOwnership(roomIdentifier: roomIdentifier)) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index f83a64973..280e98ecb 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -15,6 +15,7 @@ enum HomeScreenViewModelAction: Equatable { case presentReportRoom(roomIdentifier: String) case presentDeclineAndBlock(userID: String, roomID: String) case roomLeft(roomIdentifier: String) + case transferOwnership(roomIdentifier: String) case presentSecureBackupSettings case presentRecoveryKeyScreen case presentEncryptionResetScreen diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index b7ac69f94..cfcb4af32 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -342,15 +342,37 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol return } - if !(roomProxy.infoPublisher.value.isPrivate ?? true) { - state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isDirectOneToOneRoom, state: .public) - } else { - state.bindings.leaveRoomAlertItem = if roomProxy.infoPublisher.value.joinedMembersCount > 1 { - LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isDirectOneToOneRoom, state: .private) - } else { - LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isDirectOneToOneRoom, state: .empty) + guard roomProxy.infoPublisher.value.joinedMembersCount > 1 else { + state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, + isDM: roomProxy.isDirectOneToOneRoom, + state: roomProxy.infoPublisher.value.isPrivate ?? true ? .empty : .public) + return + } + + if !roomProxy.isDirectOneToOneRoom { + if case let .success(ownMember) = await roomProxy.getMember(userID: roomProxy.ownUserID), + ownMember.role.isOwner { + await roomProxy.updateMembers() + var isLastOwner = true + for member in roomProxy.membersPublisher.value where member.userID != roomProxy.ownUserID { + if member.role.isOwner { + isLastOwner = false + break + } + } + + if isLastOwner { + state.bindings.alertInfo = .init(id: UUID(), + title: L10n.leaveRoomAlertSelectNewOwnerTitle, + message: L10n.leaveRoomAlertSelectNewOwnerSubtitle, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.leaveRoomAlertSelectNewOwnerAction, role: .destructive, action: { [weak self] in self?.actionsSubject.send(.transferOwnership(roomIdentifier: roomID)) })) + return + } } } + + state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomID, isDM: roomProxy.isDirectOneToOneRoom, state: roomProxy.infoPublisher.value.isPrivate ?? true ? .private : .public) } } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift index 0da4b5e12..d3b57107a 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift @@ -9,7 +9,7 @@ import Combine import SwiftUI struct RoomChangeRolesScreenCoordinatorParameters { - let mode: RoomMemberDetails.Role + let mode: RoomRole let roomProxy: JoinedRoomProxyProtocol let mediaProvider: MediaProviderProtocol let userIndicatorController: UserIndicatorControllerProtocol diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenModels.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenModels.swift index 52398d7cf..104fc6284 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenModels.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenModels.swift @@ -14,15 +14,19 @@ enum RoomChangeRolesScreenViewModelAction { struct RoomChangeRolesScreenViewState: BindableState { /// The screen's current mode (which role we are promoting/demoting users to/from. - let mode: RoomMemberDetails.Role + let mode: RoomRole + /// The current user's role. + var ownRole: RoomRole + /// All of the room's members who are currently owners or creators. + var owners: [RoomMemberDetails] = [] /// All of the room's members who are currently admins. - var administrators: [RoomMemberDetails] + var administrators: [RoomMemberDetails] = [] /// All of the room's members who are currently moderators. - var moderators: [RoomMemberDetails] + var moderators: [RoomMemberDetails] = [] /// All of the room's members who are currently neither an admin or moderator. - var users: [RoomMemberDetails] + var users: [RoomMemberDetails] = [] - var bindings: RoomChangeRolesScreenViewStateBindings + var bindings = RoomChangeRolesScreenViewStateBindings() /// The members selected for promotion to the current role. var membersToPromote: Set = [] @@ -35,9 +39,17 @@ struct RoomChangeRolesScreenViewState: BindableState { /// The screen's title. var title: String { switch mode { - case .creator, .owner, .administrator: - // TODO: Handle the creator permissions change - L10n.screenRoomChangeRoleAdministratorsTitle + case .creator: + "" // The screen can't be configured with this role. + case .owner: + L10n.screenRoomChangeRoleOwnersTitle + case .administrator: + switch ownRole { + case .creator: + L10n.screenRoomChangeRoleAdministratorsOrOwnersTitle + default: + L10n.screenRoomChangeRoleAdministratorsTitle + } case .moderator: L10n.screenRoomChangeRoleModeratorsTitle case .user: @@ -45,6 +57,10 @@ struct RoomChangeRolesScreenViewState: BindableState { } } + var visibleOwners: [RoomMemberDetails] { + owners.filter { $0.matches(searchQuery: bindings.searchQuery) } + } + /// The visible admins in the screen (after searching). var visibleAdministrators: [RoomMemberDetails] { administrators.filter { $0.matches(searchQuery: bindings.searchQuery) } @@ -62,7 +78,8 @@ struct RoomChangeRolesScreenViewState: BindableState { /// All of the members who will gain/keep this screen's role after saving any changes. var membersWithRole: [RoomMemberDetails] { - administrators.filter(isMemberSelected) + moderators.filter(isMemberSelected) + users.filter(isMemberSelected) + let members = owners + administrators + moderators + users + return members.filter(isMemberSelected) } /// Whether or not any changes have been made to the members. @@ -78,7 +95,31 @@ struct RoomChangeRolesScreenViewState: BindableState { /// Whether or not a specific member has this screen's role. func isMemberSelected(_ member: RoomMemberDetails) -> Bool { guard !membersToDemote.contains(member) else { return false } - return member.role == mode || membersToPromote.contains(member) + return member.role >= mode || membersToPromote.contains(member) + } + + func isMemberDisabled(_ member: RoomMemberDetails) -> Bool { + member.role > maxDemotableRole + } + + var maxDemotableRole: RoomRole { + switch mode { + case .owner: + return .owner + case .administrator: + switch ownRole { + case .owner: + return .administrator + case .creator: + return .owner + default: + return .moderator + } + case .moderator: + return .moderator + default: + return .user + } } } @@ -91,6 +132,8 @@ struct RoomChangeRolesScreenViewStateBindings { enum RoomChangeRolesScreenAlertType { /// A warning that a particular promotion can't be undone. case promotionWarning + /// A warning that ownership transfer is final when the room is left. + case transferOwnershipWarning /// A confirmation that the user would like to discard any unsaved changes. case discardChanges /// The generic error message. diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift index 94a8833ec..5eb84aa1c 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift @@ -20,22 +20,19 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh actionsSubject.eraseToAnyPublisher() } - init(mode: RoomMemberDetails.Role, + init(mode: RoomRole, roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol, analytics: AnalyticsService) { - guard mode != .user else { fatalError("Invalid screen configuration: \(mode)") } - + guard mode != .user || mode != .creator else { fatalError("Invalid screen configuration: \(mode)") } + self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.analytics = analytics super.init(initialViewState: RoomChangeRolesScreenViewState(mode: mode, - administrators: [], - moderators: [], - users: [], - bindings: .init()), + ownRole: roomProxy.membersPublisher.value.first { $0.userID == roomProxy.id }?.role ?? .administrator), mediaProvider: mediaProvider) roomProxy.membersPublisher @@ -64,11 +61,16 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh case .demoteMember(let member): demoteMember(member) case .save: - if state.mode.isAdminOrHigher, !state.membersToPromote.isEmpty { - showPromotionWarning() - } else { - Task { await save() } + if !state.membersToPromote.isEmpty { + if state.mode == .administrator, state.ownRole == .administrator { + showPromotionWarning() + return + } else if state.mode == .owner { + showTransferOwnershipWarning() + return + } } + Task { await save() } case .cancel: confirmDiscardChanges() } @@ -77,17 +79,23 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh // MARK: - Private private func updateMembers(_ members: [RoomMemberProxyProtocol]) { + var owners = [RoomMemberDetails]() var administrators = [RoomMemberDetails]() var moderators = [RoomMemberDetails]() var users = [RoomMemberDetails]() for member in members.sorted() { + if member.userID == roomProxy.ownUserID { + state.ownRole = member.role + } + guard member.isActive else { continue } let memberDetails = RoomMemberDetails(withProxy: member) - switch member.role { - // TODO: Create a owner list - case .administrator, .creator: + switch memberDetails.role { + case .creator, .owner: + owners.append(memberDetails) + case .administrator: administrators.append(memberDetails) case .moderator: moderators.append(memberDetails) @@ -96,6 +104,7 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh } } + state.owners = owners state.administrators = administrators state.moderators = moderators state.users = users @@ -107,9 +116,9 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh } else if state.membersToDemote.contains(member) { state.membersToDemote.remove(member) state.lastPromotedMember = member - } else if member.role == state.mode { + } else if member.role >= state.mode, member.role <= state.maxDemotableRole { state.membersToDemote.insert(member) - } else { + } else if member.role < state.mode { state.membersToPromote.insert(member) state.lastPromotedMember = member } @@ -133,6 +142,16 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) } + private func showTransferOwnershipWarning() { + context.alertInfo = AlertInfo(id: .transferOwnershipWarning, + title: L10n.screenRoomChangeRoleConfirmChangeOwnersTitle, + message: L10n.screenRoomChangeRoleConfirmChangeOwnersDescription, + primaryButton: .init(title: L10n.actionContinue, role: .destructive) { + Task { await self.save() } + }, + secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + } + private func save() async { showSavingIndicator() diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift index c48a06838..5b10e2a93 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreen.swift @@ -45,15 +45,18 @@ struct RoomChangeRolesScreen: View { } } + RoomChangeRolesScreenSection(members: context.viewState.visibleOwners, + role: .owner, + context: context) + RoomChangeRolesScreenSection(members: context.viewState.visibleAdministrators, - title: L10n.screenRoomChangeRoleSectionAdministrators, - isAdministratorsSection: true, + role: .administrator, context: context) RoomChangeRolesScreenSection(members: context.viewState.visibleModerators, - title: L10n.screenRoomChangeRoleSectionModerators, + role: .moderator, context: context) RoomChangeRolesScreenSection(members: context.viewState.visibleUsers, - title: L10n.screenRoomChangeRoleSectionUsers, + role: .user, context: context) } } @@ -65,10 +68,12 @@ struct RoomChangeRolesScreen: View { ScrollViewReader { scrollView in HStack(spacing: 16) { ForEach(context.viewState.membersWithRole, id: \.id) { member in - RoomChangeRolesScreenSelectedItem(member: member, mediaProvider: context.mediaProvider) { + let dismissAction = context.viewState.isMemberDisabled(member) ? nil : { context.send(viewAction: .demoteMember(member)) } - .frame(width: cellWidth) + RoomChangeRolesScreenSelectedItem(member: member, mediaProvider: context.mediaProvider, + dismissAction: dismissAction) + .frame(width: cellWidth) } } .onChange(of: context.viewState.lastPromotedMember) { _, newValue in @@ -104,10 +109,22 @@ struct RoomChangeRolesScreen: View { // MARK: - Previews struct RoomChangeRolesScreen_Previews: PreviewProvider, TestablePreview { - static let administratorViewModel = makeViewModel(mode: .administrator) - static let moderatorViewModel = makeViewModel(mode: .moderator) + static let ownerViewModel = makeViewModel(mode: .owner, ownRole: .creator) + static let administratorOrOwnerViewModel = makeViewModel(mode: .administrator, ownRole: .creator) + static let administratorViewModel = makeViewModel(mode: .administrator, ownRole: .administrator) + static let moderatorViewModel = makeViewModel(mode: .moderator, ownRole: .administrator) static var previews: some View { + NavigationStack { + RoomChangeRolesScreen(context: ownerViewModel.context) + } + .previewDisplayName("Owners") + + NavigationStack { + RoomChangeRolesScreen(context: administratorOrOwnerViewModel.context) + } + .previewDisplayName("Administrator or Owners") + NavigationStack { RoomChangeRolesScreen(context: administratorViewModel.context) } @@ -119,11 +136,18 @@ struct RoomChangeRolesScreen_Previews: PreviewProvider, TestablePreview { .previewDisplayName("Moderators") } - static func makeViewModel(mode: RoomMemberDetails.Role) -> RoomChangeRolesScreenViewModel { - RoomChangeRolesScreenViewModel(mode: mode, - roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), - mediaProvider: MediaProviderMock(configuration: .init()), - userIndicatorController: UserIndicatorControllerMock(), - analytics: ServiceLocator.shared.analytics) + static func makeViewModel(mode: RoomRole, ownRole: RoomRole) -> RoomChangeRolesScreenViewModel { + let members: [RoomMemberProxyMock] = switch ownRole { + case .creator: + .allMembersAsCreator + default: + .allMembersAsAdminV2 + } + + return RoomChangeRolesScreenViewModel(mode: mode, + roomProxy: JoinedRoomProxyMock(.init(members: members)), + mediaProvider: MediaProviderMock(configuration: .init()), + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) } } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift index 9fde1dd0a..8c08e26c0 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSection.swift @@ -10,36 +10,46 @@ import SwiftUI struct RoomChangeRolesScreenSection: View { let members: [RoomMemberDetails] - let title: String - var isAdministratorsSection = false + let role: RoomRole let context: RoomChangeRolesScreenViewModel.Context + var title: String { + switch role { + case .creator, .owner: + L10n.screenRoomRolesAndPermissionsOwners + case .administrator: + L10n.screenRoomChangeRoleSectionAdministrators + case .moderator: + L10n.screenRoomChangeRoleSectionModerators + case .user: + L10n.screenRoomChangeRoleSectionUsers + } + } + var body: some View { if !members.isEmpty { Section { ForEach(members, id: \.id) { member in RoomChangeRolesScreenRow(member: member, mediaProvider: context.mediaProvider, - isSelected: isMemberSelected(member)) { + isSelected: context.viewState.isMemberSelected(member)) { context.send(viewAction: .toggleMember(member)) } - .disabled(member.role.isAdminOrHigher) + .disabled(context.viewState.isMemberDisabled(member)) } } header: { Text(title) .compoundListSectionHeader() } footer: { - if isAdministratorsSection, context.viewState.mode == .moderator { + if role == .administrator, context.viewState.mode == .moderator { Text(L10n.screenRoomChangeRoleModeratorsAdminSectionFooter) .compoundListSectionFooter() + } else if role.isOwner, context.viewState.mode != .owner { + Text(L10n.screenRoomChangeRoleModeratorsOwnerSectionFooter) + .compoundListSectionFooter() } } } } - - private func isMemberSelected(_ member: RoomMemberDetails) -> Bool { - // We always show administrators as selected, even on the moderators screen. - member.role.isAdminOrHigher || context.viewState.isMemberSelected(member) - } } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift index f33c7882c..0d8ca8ebf 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/View/RoomChangeRolesScreenSelectedItem.swift @@ -10,9 +10,22 @@ import SwiftUI struct RoomChangeRolesScreenSelectedItem: View { let member: RoomMemberDetails let mediaProvider: MediaProviderProtocol? - let dismissAction: () -> Void + let dismissAction: (() -> Void)? var body: some View { + mainContent + .accessibilityActions { + if let dismissAction { + Button(L10n.actionDismiss) { + dismissAction() + } + } + } + } + + // MARK: - Private + + private var mainContent: some View { VStack(spacing: 4) { avatar @@ -22,13 +35,8 @@ struct RoomChangeRolesScreenSelectedItem: View { .lineLimit(1) } .accessibilityElement(children: .combine) - .accessibilityAction(named: L10n.actionDismiss) { - dismissAction() - } } - // MARK: - Private - var avatar: some View { LoadableAvatarImage(url: member.avatarURL, name: member.name, @@ -37,7 +45,7 @@ struct RoomChangeRolesScreenSelectedItem: View { mediaProvider: mediaProvider) .accessibilityHidden(true) .overlay(alignment: .topTrailing) { - if member.role != .administrator { + if let dismissAction { Button(action: dismissAction) { Image(systemName: "xmark.circle.fill") .resizable() diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index 8a4bda3d7..0de434286 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -34,6 +34,7 @@ enum RoomDetailsScreenCoordinatorAction { case presentKnockingRequestsListScreen case presentSecurityAndPrivacyScreen case presentReportRoomScreen + case transferOwnership } final class RoomDetailsScreenCoordinator: CoordinatorProtocol { @@ -94,6 +95,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.presentRecipientDetails(userID: userID)) case .displayReportRoom: actionsSubject.send(.presentReportRoomScreen) + case .transferOwnership: + actionsSubject.send(.transferOwnership) } } .store(in: &cancellables) diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 2e2f45680..3c85eef5e 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -27,6 +27,7 @@ enum RoomDetailsScreenViewModelAction: Equatable { case displayKnockingRequests case displaySecurityAndPrivacy case displayReportRoom + case transferOwnership } // MARK: View @@ -288,6 +289,8 @@ enum RoomDetailsScreenErrorType: Hashable { case alert /// Leaving room has failed.. case unknown + /// Last owner + case lastOwner } enum RoomDetailsScreenPinnedEventsActionState { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index 16e96f0be..be6fe8333 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -119,13 +119,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr case .processTapInvite: actionsSubject.send(.requestInvitePeoplePresentation) case .processTapLeave: - guard state.joinedMembersCount > 1 else { - state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, isDM: roomProxy.isDirectOneToOneRoom, state: .empty) - return - } - state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, - isDM: roomProxy.isDirectOneToOneRoom, - state: roomProxy.infoPublisher.value.isPrivate ?? true ? .private : .public) + processTapToLeave() case .confirmLeave: Task { await leaveRoom() } case .processTapIgnore: @@ -177,6 +171,38 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr // MARK: - Private + private func processTapToLeave() { + guard state.joinedMembersCount > 1 else { + state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, + isDM: roomProxy.isDirectOneToOneRoom, + state: roomProxy.infoPublisher.value.isPrivate ?? true ? .empty : .public) + return + } + + if !roomProxy.isDirectOneToOneRoom, state.accountOwner?.role.isOwner == true { + var isLastOwner = true + for member in roomProxy.membersPublisher.value where member.userID != roomProxy.ownUserID { + if member.role.isOwner { + isLastOwner = false + break + } + } + + if isLastOwner { + state.bindings.alertInfo = .init(id: .lastOwner, + title: L10n.leaveRoomAlertSelectNewOwnerTitle, + message: L10n.leaveRoomAlertSelectNewOwnerSubtitle, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + secondaryButton: .init(title: L10n.leaveRoomAlertSelectNewOwnerAction, role: .destructive, action: { [weak self] in self?.actionsSubject.send(.transferOwnership) })) + return + } + } + + state.bindings.leaveRoomAlertItem = LeaveRoomAlertItem(roomID: roomProxy.id, + isDM: roomProxy.isDirectOneToOneRoom, + state: roomProxy.infoPublisher.value.isPrivate ?? true ? .private : .public) + } + private func setupRoomSubscription() { roomProxy.infoPublisher .receive(on: DispatchQueue.main) @@ -237,11 +263,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } private func fetchMembersIfNeeded() async { - // We need to fetch members just in 1-to-1 chat to get the member object for the other person - guard roomProxy.isDirectOneToOneRoom else { - return - } - roomProxy.membersPublisher .receive(on: DispatchQueue.main) .sink { [weak self, ownUserID = roomProxy.ownUserID] members in @@ -251,6 +272,10 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr self.state.accountOwner = .init(withProxy: accountOwner) } + guard roomProxy.isDirectOneToOneRoom else { + return + } + if let dmRecipient = members.first(where: { $0.userID != ownUserID }) { self.state.dmRecipientInfo = .init(member: .init(withProxy: dmRecipient)) diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift index 9420d8a33..78e038bbb 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift @@ -17,12 +17,16 @@ enum RoomRolesAndPermissionsScreenViewModelAction { } struct RoomRolesAndPermissionsScreenViewState: BindableState { + var ownRole: RoomRole + + var administratorsAndOwnersCount: Int? /// The number of administrators in the room. var administratorCount: Int? /// The number of moderators in the room. var moderatorCount: Int? /// The permissions of the room when loaded. var permissions: RoomPermissions? + var bindings = RoomRolesAndPermissionsScreenViewStateBindings() } @@ -46,7 +50,7 @@ enum RoomRolesAndPermissionsScreenViewAction { case reset } -enum RoomRolesAndPermissionsScreenRole { +enum RoomRolesAndPermissionsScreenRole: Hashable { case administrators case moderators } diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index 0154d5dec..7562a1e89 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -14,6 +14,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM private let roomProxy: JoinedRoomProxyProtocol private let userIndicatorController: UserIndicatorControllerProtocol private let analytics: AnalyticsService + private var ownUser: RoomMemberDetails? private var actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { @@ -24,7 +25,8 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.analytics = analytics - super.init(initialViewState: RoomRolesAndPermissionsScreenViewState(permissions: initialPermissions)) + super.init(initialViewState: RoomRolesAndPermissionsScreenViewState(ownRole: roomProxy.membersPublisher.value.first(where: { $0.userID == roomProxy.ownUserID })?.role ?? .administrator, + permissions: initialPermissions)) // Automatically update the admin/moderator counts. roomProxy.membersPublisher @@ -80,16 +82,19 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM secondaryButton: .init(title: L10n.actionCancel, role: .cancel) { }) } } - + // MARK: - Members private func updateMembers(_ members: [RoomMemberProxyProtocol]) { - // TODO: Will probably be changed when we implement the owners list - state.administratorCount = members.filter { $0.role.isAdminOrHigher && $0.isActive }.count + state.administratorsAndOwnersCount = members.filter { $0.role.isAdminOrHigher && $0.isActive }.count + state.administratorCount = members.filter { $0.role == .administrator && $0.isActive }.count state.moderatorCount = members.filter { $0.role == .moderator && $0.isActive }.count + if let ownUser = members.first(where: { $0.userID == roomProxy.ownUserID }) { + state.ownRole = ownUser.role + } } - private func updateOwnRole(_ role: RoomMemberDetails.Role) async { + private func updateOwnRole(_ role: RoomRole) async { showSavingIndicator() // A task we can await until the room's info gets modified with the new power levels. @@ -142,7 +147,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM hideSavingIndicator() } - + // MARK: Loading indicator private static let savingIndicatorID = "RolesAndPermissionsSaving" diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift index e4a0cb14a..261c8e670 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/View/RoomRolesAndPermissionsScreen.swift @@ -26,13 +26,23 @@ struct RoomRolesAndPermissionsScreen: View { private var rolesSection: some View { Section { - ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsAdmins, - icon: \.admin), - details: administratorDetails, - kind: .navigationLink { - context.send(viewAction: .editRoles(.administrators)) - }) - .accessibilityIdentifier(A11yIdentifiers.roomRolesAndPermissionsScreen.administrators) + if context.viewState.ownRole == .creator { + ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsAdminsAndOwners, + icon: \.admin), + details: administratorOrOwnersDetails, + kind: .navigationLink { + context.send(viewAction: .editRoles(.administrators)) + }) + .accessibilityIdentifier(A11yIdentifiers.roomRolesAndPermissionsScreen.administrators) + } else { + ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsAdmins, + icon: \.admin), + details: administratorDetails, + kind: .navigationLink { + context.send(viewAction: .editRoles(.administrators)) + }) + .accessibilityIdentifier(A11yIdentifiers.roomRolesAndPermissionsScreen.administrators) + } ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsModerators, icon: \.chatProblem), @@ -42,17 +52,27 @@ struct RoomRolesAndPermissionsScreen: View { }) .accessibilityIdentifier(A11yIdentifiers.roomRolesAndPermissionsScreen.moderators) - ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsChangeMyRole, - icon: \.edit), - kind: .button { - context.send(viewAction: .editOwnUserRole) - }) + if context.viewState.ownRole != .creator { + ListRow(label: .default(title: L10n.screenRoomRolesAndPermissionsChangeMyRole, + icon: \.edit), + kind: .button { + context.send(viewAction: .editOwnUserRole) + }) + } } header: { Text(L10n.screenRoomRolesAndPermissionsRolesHeader) .compoundListSectionHeader() } } + private var administratorOrOwnersDetails: ListRowDetails { + if let administratorCount = context.viewState.administratorsAndOwnersCount { + .title("\(administratorCount)") + } else { + .isWaiting(true) + } + } + private var administratorDetails: ListRowDetails { if let administratorCount = context.viewState.administratorCount { .title("\(administratorCount)") @@ -121,9 +141,20 @@ struct RoomRolesAndPermissionsScreen_Previews: PreviewProvider, TestablePreview roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)), userIndicatorController: UserIndicatorControllerMock(), analytics: ServiceLocator.shared.analytics) + + static let creatorViewModel = RoomRolesAndPermissionsScreenViewModel(initialPermissions: RoomPermissions(powerLevels: .mock), + roomProxy: JoinedRoomProxyMock(.init(members: .allMembersAsCreator)), + userIndicatorController: UserIndicatorControllerMock(), + analytics: ServiceLocator.shared.analytics) static var previews: some View { NavigationStack { RoomRolesAndPermissionsScreen(context: viewModel.context) } + .previewDisplayName("Admin") + + NavigationStack { + RoomRolesAndPermissionsScreen(context: creatorViewModel.context) + } + .previewDisplayName("Creator") } } diff --git a/ElementX/Sources/Services/Analytics/AnalyticsService.swift b/ElementX/Sources/Services/Analytics/AnalyticsService.swift index fb61d5e25..d532ccfaf 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsService.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsService.swift @@ -211,7 +211,7 @@ extension AnalyticsService { } /// Track a room moderation action. - func trackRoomModeration(action: AnalyticsEvent.RoomModeration.Action, role: RoomMemberDetails.Role?) { + func trackRoomModeration(action: AnalyticsEvent.RoomModeration.Action, role: RoomRole?) { let role = role.map(AnalyticsEvent.RoomModeration.Role.init) capture(event: AnalyticsEvent.RoomModeration(action: action, role: role)) } diff --git a/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift b/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift index 981f69532..9b6dc4ef7 100644 --- a/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift +++ b/ElementX/Sources/Services/Analytics/Helpers/RoomModerationRole.swift @@ -8,7 +8,7 @@ import AnalyticsEvents extension AnalyticsEvent.RoomModeration.Role { - init(role: RoomMemberDetails.Role) { + init(role: RoomRole) { switch role { case .administrator, .creator, .owner: // This probably needs to be updates diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift index 1aa565023..844eeacc3 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift @@ -18,21 +18,8 @@ struct RoomMemberDetails: Identifiable, Hashable { var isIgnored: Bool var isBanned: Bool var isActive: Bool - - enum Role { - /// Creator of the room, PL infinite - case creator - /// Same power of an admin, but they can also upgrade the room, PL 150 onwards - case owner - /// Able to edit room settings and perform any action aside from room upgrading PL 100...149 - case administrator - /// Able to perform room moderation actions PL 50...99 - case moderator - /// Default role PL 0...49 - case user - } - let role: Role + let role: RoomRole let powerLevel: RoomPowerLevel func matches(searchQuery: String) -> Bool { @@ -51,47 +38,7 @@ extension RoomMemberDetails { isInvited = proxy.membership == .invite isIgnored = proxy.isIgnored isBanned = proxy.membership == .ban - role = .init(proxy.role, powerLevel: proxy.powerLevel) + role = proxy.role powerLevel = proxy.powerLevel } } - -extension RoomMemberDetails.Role { - init(_ role: RoomMemberRole, powerLevel: RoomPowerLevel) { - switch role { - case .creator: - self = .creator - case .administrator: - switch powerLevel { - case .value(let value): - self = value >= 150 ? .owner : .administrator - default: - fatalError("Impossible") - } - case .moderator: - self = .moderator - case .user: - self = .user - } - } - - var isAdminOrHigher: Bool { - switch self { - case .administrator, .creator, .owner: - return true - case .moderator, .user: - return false - } - } -} - -extension RoomMemberRole { - var isAdminOrHigher: Bool { - switch self { - case .administrator, .creator: - return true - case .moderator, .user: - return false - } - } -} diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift index 0bbfc6ba0..6347c5041 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxy.swift @@ -36,6 +36,4 @@ final class RoomMemberProxy: RoomMemberProxyProtocol { var isIgnored: Bool { member.isIgnored } var powerLevel: RoomPowerLevel { .init(rustPowerLevel: member.powerLevel) } - - var role: RoomMemberRole { member.suggestedRoleForPowerLevel } } diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift index f37f95aee..f14dc8e48 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberProxyProtocol.swift @@ -24,8 +24,6 @@ protocol RoomMemberProxyProtocol: AnyObject { var isIgnored: Bool { get } var powerLevel: RoomPowerLevel { get } - - var role: RoomMemberRole { get } } extension RoomMemberProxyProtocol { @@ -58,3 +56,7 @@ extension [RoomMemberProxyProtocol] { } } } + +extension RoomMemberProxyProtocol { + var role: RoomRole { .init(powerLevel: powerLevel) } +} diff --git a/ElementX/Sources/Services/Room/RoomPermissions.swift b/ElementX/Sources/Services/Room/RoomPermissions.swift index 3dfde378e..db7c0139b 100644 --- a/ElementX/Sources/Services/Room/RoomPermissions.swift +++ b/ElementX/Sources/Services/Room/RoomPermissions.swift @@ -9,15 +9,15 @@ import Foundation import MatrixRustSDK struct RoomPermissionsSetting: Identifiable { - var id: KeyPath { keyPath } + var id: KeyPath { keyPath } /// The title of this setting. let title: String /// The selected role of this setting. - var value: RoomMemberDetails.Role + var value: RoomRole /// All of the available roles that this setting can be configured with. - var allValues: [(title: String, tag: RoomMemberDetails.Role)] { + var allValues: [(title: String, tag: RoomRole)] { [ (title: L10n.screenRoomChangePermissionsAdministrators, tag: .administrator), (title: L10n.screenRoomChangePermissionsModerators, tag: .moderator), @@ -26,7 +26,7 @@ struct RoomPermissionsSetting: Identifiable { } /// The `RoomPermissions` property that this setting is for. - let keyPath: KeyPath + let keyPath: KeyPath /// The `RoomPowerLevelChanges` property that this setting is saved into. var rustKeyPath: WritableKeyPath { switch keyPath { @@ -47,99 +47,39 @@ struct RoomPermissionsSetting: Identifiable { struct RoomPermissions { /// The level required to ban a user. - var ban: RoomMemberDetails.Role + var ban: RoomRole /// The level required to invite a user. - var invite: RoomMemberDetails.Role + var invite: RoomRole /// The level required to kick a user. - var kick: RoomMemberDetails.Role + var kick: RoomRole /// The level required to redact an event. - var redact: RoomMemberDetails.Role + var redact: RoomRole /// The default level required to send message events. - var eventsDefault: RoomMemberDetails.Role + var eventsDefault: RoomRole /// The default level required to send state events. - var stateDefault: RoomMemberDetails.Role + var stateDefault: RoomRole /// The default power level for every user in the room. - var usersDefault: RoomMemberDetails.Role + var usersDefault: RoomRole /// The level required to change the room's name. - var roomName: RoomMemberDetails.Role + var roomName: RoomRole /// The level required to change the room's avatar. - var roomAvatar: RoomMemberDetails.Role + var roomAvatar: RoomRole /// The level required to change the room's topic. - var roomTopic: RoomMemberDetails.Role + var roomTopic: RoomRole } extension RoomPermissions { /// Create permissions from the room's power levels. init(powerLevels: RoomPowerLevelsValues) { - ban = RoomMemberDetails.Role(powerLevelValue: powerLevels.ban) - invite = RoomMemberDetails.Role(powerLevelValue: powerLevels.invite) - kick = RoomMemberDetails.Role(powerLevelValue: powerLevels.kick) - redact = RoomMemberDetails.Role(powerLevelValue: powerLevels.redact) - eventsDefault = RoomMemberDetails.Role(powerLevelValue: powerLevels.eventsDefault) - stateDefault = RoomMemberDetails.Role(powerLevelValue: powerLevels.stateDefault) - usersDefault = RoomMemberDetails.Role(powerLevelValue: powerLevels.usersDefault) - roomName = RoomMemberDetails.Role(powerLevelValue: powerLevels.roomName) - roomAvatar = RoomMemberDetails.Role(powerLevelValue: powerLevels.roomAvatar) - roomTopic = RoomMemberDetails.Role(powerLevelValue: powerLevels.roomTopic) - } -} - -extension RoomMemberDetails.Role { - init(powerLevelValue: Int64) { - // Also this is not great, and should be handled by a `suggestedRoleForPowerLevelValue` function from the SDK - guard powerLevelValue < 150 else { - self = .owner - return - } - - do { - switch try suggestedRoleForPowerLevel(powerLevel: .value(value: powerLevelValue)) { - case .administrator: - self = .administrator - case .creator: - fatalError("Impossible") - case .moderator: - self = .moderator - case .user: - self = .user - } - } catch { - MXLog.error("Falied to convert power level value to role: \(error)") - self = .user - } - } - - var rustRole: RoomMemberRole { - switch self { - case .creator: - .creator - case .administrator, .owner: - .administrator - case .moderator: - .moderator - case .user: - .user - } - } - - /// To be used when setting the power level of a user to get the suggested equivalent power level value for that specific role - /// NOTE: Do not use for comparison, use the true power level instead. - var powerLevelValue: Int64 { - guard self != .owner else { - // Would be better if the SDK would return this, maybe a `suggestedPowerLevelValueForRole` function would solve the problem - return 150 - } - - do { - switch try suggestedPowerLevelForRole(role: rustRole) { - case .infinite: - fatalError("Impossible") - case .value(let value): - return value - } - } catch { - MXLog.error("Falied to convert role to power level value: \(error)") - return 0 - } + ban = RoomRole(powerLevelValue: powerLevels.ban) + invite = RoomRole(powerLevelValue: powerLevels.invite) + kick = RoomRole(powerLevelValue: powerLevels.kick) + redact = RoomRole(powerLevelValue: powerLevels.redact) + eventsDefault = RoomRole(powerLevelValue: powerLevels.eventsDefault) + stateDefault = RoomRole(powerLevelValue: powerLevels.stateDefault) + usersDefault = RoomRole(powerLevelValue: powerLevels.usersDefault) + roomName = RoomRole(powerLevelValue: powerLevels.roomName) + roomAvatar = RoomRole(powerLevelValue: powerLevels.roomAvatar) + roomTopic = RoomRole(powerLevelValue: powerLevels.roomTopic) } } diff --git a/ElementX/Sources/Services/Room/RoomRole.swift b/ElementX/Sources/Services/Room/RoomRole.swift new file mode 100644 index 000000000..326eb2885 --- /dev/null +++ b/ElementX/Sources/Services/Room/RoomRole.swift @@ -0,0 +1,129 @@ +// +// 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 MatrixRustSDK + +enum RoomRole: Comparable { + /// Default role PL 0...49 + case user + /// Able to perform room moderation actions PL 50...99 + case moderator + /// Able to edit room settings and perform any action aside from room upgrading PL 100...149 + case administrator + /// Same power of an admin, but they can also upgrade the room, PL 150 onwards + case owner + /// Creator of the room, PL infinite + case creator +} + +extension RoomRole { + init(powerLevel: RoomPowerLevel) { + do { + let role = try suggestedRoleForPowerLevel(powerLevel: powerLevel.rustPowerLevel) + self.init(role, powerLevel: powerLevel) + } catch { + MXLog.error("Failled to get suggested role for power level \(powerLevel): \(error)") + self = .user + } + } + + init(_ role: RoomMemberRole, powerLevel: RoomPowerLevel) { + switch role { + case .creator: + self = .creator + case .administrator: + switch powerLevel { + case .value(let value): + self = value >= 150 ? .owner : .administrator + default: + fatalError("Impossible") + } + case .moderator: + self = .moderator + case .user: + self = .user + } + } + + var isAdminOrHigher: Bool { + switch self { + case .administrator, .creator, .owner: + return true + case .moderator, .user: + return false + } + } + + var isOwner: Bool { + switch self { + case .creator, .owner: + return true + case .administrator, .moderator, .user: + return false + } + } +} + +extension RoomRole { + init(powerLevelValue: Int64) { + // Also this is not great, and should be handled by a `suggestedRoleForPowerLevelValue` function from the SDK + guard powerLevelValue < 150 else { + self = .owner + return + } + + do { + switch try suggestedRoleForPowerLevel(powerLevel: .value(value: powerLevelValue)) { + case .administrator: + self = .administrator + case .creator: + fatalError("Impossible") + case .moderator: + self = .moderator + case .user: + self = .user + } + } catch { + MXLog.error("Falied to convert power level value to role: \(error)") + self = .user + } + } + + var rustRole: RoomMemberRole { + switch self { + case .creator: + .creator + case .administrator, .owner: + .administrator + case .moderator: + .moderator + case .user: + .user + } + } + + /// To be used when setting the power level of a user to get the suggested equivalent power level value for that specific role + /// NOTE: Do not use for comparison, use the true power level instead. + var powerLevelValue: Int64 { + guard self != .owner else { + // Would be better if the SDK would return this, maybe a `suggestedPowerLevelValueForRole` function would solve the problem + return 150 + } + + do { + switch try suggestedPowerLevelForRole(role: rustRole) { + case .infinite: + fatalError("Impossible") + case .value(let value): + return value + } + } catch { + MXLog.error("Falied to convert role to power level value: \(error)") + return 0 + } + } +} diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-en-GB.png new file mode 100644 index 000000000..43e34b87c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df90344bb87a1b2f65e85cc65c749054dacac02c0c634501484bc02a7a9e6429 +size 226433 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-pseudo.png new file mode 100644 index 000000000..61bf5bc84 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:caa50aaa716140d4613e6682fa1facb12cc57f6bf6ee8e29e801ac5a5ab03db9 +size 238373 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-en-GB.png new file mode 100644 index 000000000..35dd66769 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d40cb24ec76d87e4f8b0c4ead60a4fab50d8cc24ee0c903e527ddfaa5896b60 +size 170864 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-pseudo.png new file mode 100644 index 000000000..c0477a5e5 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrator-or-Owners-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7639e4fa100e48262f35e996caa31cf9a67c0f10cb73a4be8c2fc64bf70ee8bf +size 180516 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-en-GB.png index e0db271ef..e9e54b17b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45bfc56242e51815e312057b7603d19c3f2ae488f81145f6e8c45bdd405bafd4 -size 219778 +oid sha256:356954ba65b0fe34441df8440182ab743df30aa9f6c6932bb9b5107ae87cb839 +size 221488 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-pseudo.png index 62f96d20d..499b6a07f 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef779c260c378cb37d32e19aa9fb58c73818aca7153cdb762078487e6245b8cb -size 226361 +oid sha256:7ce7e57ce7b90e91395dd3e0c9b3a73d944ac43a8f36feb77fb106e9f86add67 +size 231973 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-en-GB.png index fc7e2a0a6..e4439f43b 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08b1ed25327b18ce7be7831bf62644d25269b7204d79fb1058466893923f3559 -size 172593 +oid sha256:28dd538e435dc4125d34c8cbf45c5403a4dc1b4759d3d16cb0e56f4b1bb5a63f +size 167867 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-pseudo.png index 8d43cff8f..3cfef6034 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Administrators-iPhone-16-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22561fa01da4cff09515ed5bf9c70beb55b2c691bf86c911aedc0090bef1396c -size 178152 +oid sha256:d56fb49526da5e418e3311a8ab121efcde6cc5dd2234c3662997884e0f2e6991 +size 176485 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-en-GB.png index 6e4bd2347..13ec0de7d 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06194fbeb1d19db9882525340b7a73d6cbae649c9a13f1b527e1d26306946c40 -size 184357 +oid sha256:5d7ee9d49212820a88171df680a31ad658ac62ae7fd3474a24403c130a4630d8 +size 231534 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-pseudo.png index 16beaaa3e..ebb1cae73 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPad-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a14dd1411077dd6f20e46c67a8856169f04bc186bf7015ebe5fa37cdceb8981 -size 195057 +oid sha256:312f9f0d95ae3dd31c2c8ab4ce37a9de51febdd79a9d65b60205241e5a18f1cb +size 247092 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-en-GB.png index da32f9dad..07960a1c2 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-en-GB.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-en-GB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6382218fc0c357a9b4cabcbfb737e4678d2f52f89949a9ddab5301f3c70023fc -size 144983 +oid sha256:ddc42a0c0de638440189c23ec7ef85c9b84ea2fdbc8e222994adbc5f4ab88d5a +size 172583 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-pseudo.png index 145a65121..58bd0ea52 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-pseudo.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Moderators-iPhone-16-pseudo.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2014f024ade0cb7901089e7637f7136474d057eb8dbb84577deecc7d3d16bb -size 154055 +oid sha256:47fa5b57b70a18e75a9b690cc96e5bb3c7db26ccd06eae05f3d3e6f8bbd92647 +size 188261 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-en-GB.png new file mode 100644 index 000000000..cebf32f76 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dde88bcec2a8cc78858a4513f23a9b73aeab2478e52d730823e451e486e6d082 +size 217524 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-pseudo.png new file mode 100644 index 000000000..01b134d3e --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37a45dbaf05d8dd1ad69d625aadcd9291efd5fecc490f267b72a60f903ec5986 +size 224360 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-en-GB.png new file mode 100644 index 000000000..14eed2b09 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8e6022a3382c727169489bb965bf7ba2681417f4fb3123b4043745ca8887046 +size 161674 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-pseudo.png new file mode 100644 index 000000000..f132a5704 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreen.Owners-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:484250de28a8f3804c89cf551cba3903675c6e2fd28ce80e4b25145a674b430b +size 168264 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-en-GB-0.png index 0485021b6..5498b082e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:673f640cc1b7368d7c4bb70d5b5ac325941723cbf740507a0611606eee50d373 -size 62443 +oid sha256:60efb65986ae484b7f7da9648db252ab4aa11e0047020574244e070972813f0a +size 62712 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-pseudo-0.png index 0485021b6..5498b082e 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPad-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:673f640cc1b7368d7c4bb70d5b5ac325941723cbf740507a0611606eee50d373 -size 62443 +oid sha256:60efb65986ae484b7f7da9648db252ab4aa11e0047020574244e070972813f0a +size 62712 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-en-GB-0.png index 6d6eef873..08ce73f3c 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-en-GB-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-en-GB-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d07e63d1d9dd2a676e05c95c3562e7e2ef397d9cb2ac07f8c5ca0c3eb510024 -size 52234 +oid sha256:e9f33cdbe4d65566be99949b00a54439b90d4d010ec9fb9af505fbc043271177 +size 52462 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-pseudo-0.png index 6d6eef873..08ce73f3c 100644 --- a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-pseudo-0.png +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomChangeRolesScreenSelectedItem.iPhone-16-pseudo-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d07e63d1d9dd2a676e05c95c3562e7e2ef397d9cb2ac07f8c5ca0c3eb510024 -size 52234 +oid sha256:e9f33cdbe4d65566be99949b00a54439b90d4d010ec9fb9af505fbc043271177 +size 52462 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPad-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPad-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPad-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPad-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPad-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPad-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPad-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPad-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPhone-16-en-GB-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPhone-16-en-GB.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPhone-16-en-GB-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPhone-16-en-GB.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPhone-16-pseudo-0.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPhone-16-pseudo.png similarity index 100% rename from PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.iPhone-16-pseudo-0.png rename to PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Admin-iPhone-16-pseudo.png diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-en-GB.png new file mode 100644 index 000000000..56a8f705a --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c89484df5502aa6935e8c6babdcceb49b6701fbb4099437046bcffc22764ec1 +size 129589 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-pseudo.png new file mode 100644 index 000000000..9cc193b4c --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPad-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:206708dbbb48e6f68d21b98103fd8600ca595395b0f3695a28560bf48a4a7ff8 +size 139089 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-en-GB.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-en-GB.png new file mode 100644 index 000000000..864a3c0ca --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-en-GB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42f5682c78a9786db0ca354c284b369d666cec7136682e82edc01922d48a2d68 +size 79075 diff --git a/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-pseudo.png b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-pseudo.png new file mode 100644 index 000000000..da4d97848 --- /dev/null +++ b/PreviewTests/Sources/__Snapshots__/PreviewTests/roomRolesAndPermissionsScreen.Creator-iPhone-16-pseudo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df0d7583c6493dd81930c92f23c74e60467743a94bada42ecf6fb17121ccb117 +size 100746 diff --git a/UnitTests/Sources/LocalizationTests.swift b/UnitTests/Sources/LocalizationTests.swift index 15b41d436..a727fcd48 100644 --- a/UnitTests/Sources/LocalizationTests.swift +++ b/UnitTests/Sources/LocalizationTests.swift @@ -49,21 +49,14 @@ class LocalizationTests: XCTestCase { // set app language to English Bundle.overrideLocalizations = ["en"] - XCTAssertEqual(L10n.commonMemberCount(1), "1 member") - XCTAssertEqual(L10n.commonMemberCount(2), "2 members") + XCTAssertEqual(L10n.commonMemberCount(1), "1 Member") + XCTAssertEqual(L10n.commonMemberCount(2), "2 Members") // set app language to Italian Bundle.overrideLocalizations = ["it"] - XCTAssertEqual(L10n.commonMemberCount(1), "1 membro") - XCTAssertEqual(L10n.commonMemberCount(2), "2 membri") - -// // set app language to Polish -// Bundle.overrideLocalizations = ["pl"] -// -// XCTAssertEqual(L10n.commonMemberCount(1), "1 sekunda") // one -// XCTAssertEqual(L10n.commonMemberCount(2), "2 sekundy") // few -// XCTAssertEqual(L10n.commonMemberCount(3), "5 sekund") // many, other + XCTAssertEqual(L10n.commonMemberCount(1), "1 Membro") + XCTAssertEqual(L10n.commonMemberCount(2), "2 Membri") } /// Test plurals fallback language for a language not supported at all @@ -71,8 +64,8 @@ class LocalizationTests: XCTestCase { // set app language to something Element don't support at all ("invalid identifier") Bundle.overrideLocalizations = ["xx"] - XCTAssertEqual(L10n.commonMemberCount(1), "1 member") - XCTAssertEqual(L10n.commonMemberCount(2), "2 members") + XCTAssertEqual(L10n.commonMemberCount(1), "1 Member") + XCTAssertEqual(L10n.commonMemberCount(2), "2 Members") } /// Test untranslated strings diff --git a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift index 8059cf848..64f2f3ba1 100644 --- a/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangeRolesScreenViewModelTests.swift @@ -38,8 +38,8 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.administrators, context.viewState.visibleAdministrators) XCTAssertEqual(context.viewState.moderators, context.viewState.visibleModerators) XCTAssertEqual(context.viewState.users, context.viewState.visibleUsers) - XCTAssertEqual(context.viewState.membersWithRole.count, 1) - XCTAssertEqual(context.viewState.membersWithRole.first?.id, RoomMemberProxyMock.mockModerator.userID) + XCTAssertEqual(context.viewState.membersWithRole.count, 3) + XCTAssert(context.viewState.membersWithRole.first(where: { $0.id == RoomMemberProxyMock.mockModerator.userID }) != nil) XCTAssertFalse(context.viewState.hasChanges) XCTAssertFalse(context.viewState.isSearching) } @@ -55,7 +55,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.membersToPromote, [firstUser]) XCTAssertEqual(context.viewState.membersToDemote, []) - XCTAssertEqual(context.viewState.membersWithRole.count, 2) + XCTAssertEqual(context.viewState.membersWithRole.count, 4) XCTAssertTrue(context.viewState.membersWithRole.contains(firstUser)) XCTAssertTrue(context.viewState.hasChanges) } @@ -71,7 +71,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, []) - XCTAssertEqual(context.viewState.membersWithRole.count, 1) + XCTAssertEqual(context.viewState.membersWithRole.count, 3) XCTAssertFalse(context.viewState.membersWithRole.contains(firstUser)) XCTAssertFalse(context.viewState.hasChanges) } @@ -87,14 +87,14 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, []) - XCTAssertEqual(context.viewState.membersWithRole.count, 1) + XCTAssertEqual(context.viewState.membersWithRole.count, 3) XCTAssertFalse(context.viewState.membersWithRole.contains(firstUser)) XCTAssertFalse(context.viewState.hasChanges) } func testToggleModeratorOff() { testInitialStateModerators() - guard let existingModerator = context.viewState.membersWithRole.first else { + guard let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else { XCTFail("There should be a member with the role before we begin.") return } @@ -103,7 +103,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, [existingModerator]) - XCTAssertEqual(context.viewState.membersWithRole.count, 0) + XCTAssertEqual(context.viewState.membersWithRole.count, 2) XCTAssertFalse(context.viewState.membersWithRole.contains(existingModerator)) XCTAssertTrue(context.viewState.hasChanges) } @@ -120,14 +120,14 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, []) - XCTAssertEqual(context.viewState.membersWithRole.count, 1) + XCTAssertEqual(context.viewState.membersWithRole.count, 3) XCTAssertTrue(context.viewState.membersWithRole.contains(demotedMember)) XCTAssertFalse(context.viewState.hasChanges) } func testDemoteModerator() { testInitialStateModerators() - guard let existingModerator = context.viewState.membersWithRole.first else { + guard let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else { XCTFail("There should be a member with the role before we begin.") return } @@ -136,7 +136,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(context.viewState.membersToPromote, []) XCTAssertEqual(context.viewState.membersToDemote, [existingModerator]) - XCTAssertEqual(context.viewState.membersWithRole.count, 0) + XCTAssertEqual(context.viewState.membersWithRole.count, 2) XCTAssertFalse(context.viewState.membersWithRole.contains(existingModerator)) XCTAssertTrue(context.viewState.hasChanges) } @@ -146,7 +146,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { setupViewModel(mode: .moderator) guard let firstUser = context.viewState.users.first(where: { !context.viewState.isMemberSelected($0) }), - let existingModerator = context.viewState.membersWithRole.first else { + let existingModerator = context.viewState.membersWithRole.first(where: { $0.role == .moderator }) else { XCTFail("There should be a regular user and a moderator to begin with.") return } @@ -192,7 +192,7 @@ class RoomChangeRolesScreenViewModelTests: XCTestCase { XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.contains { $0.userID == firstUser.id && $0.powerLevel == 100 }, true) } - private func setupViewModel(mode: RoomMemberDetails.Role) { + private func setupViewModel(mode: RoomRole) { roomProxy = JoinedRoomProxyMock(.init(members: .allMembersAsAdmin)) viewModel = RoomChangeRolesScreenViewModel(mode: mode, roomProxy: roomProxy, diff --git a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift index aef3cf289..f9681935e 100644 --- a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift @@ -55,7 +55,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel, - RoomMemberDetails.Role.moderator.powerLevelValue) + RoomRole.moderator.powerLevelValue) } func testDemoteToMember() async throws { @@ -70,7 +70,7 @@ class RoomRolesAndPermissionsScreenViewModelTests: XCTestCase { XCTAssertTrue(roomProxy.updatePowerLevelsForUsersCalled) XCTAssertEqual(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel, - RoomMemberDetails.Role.user.powerLevelValue) + RoomRole.user.powerLevelValue) } private func setupViewModel(members: [RoomMemberProxyMock]) {