Last Owner should edit admins, and not leave when is last (#4372)

This commit is contained in:
Mauro
2025-08-07 10:40:14 +02:00
committed by GitHub
parent be525e6a60
commit 5ab6f90863
67 changed files with 691 additions and 363 deletions
+25 -38
View File
@@ -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 = "<group>"; };
045253F9967A535EE5B16691 /* Label.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Label.swift; sourceTree = "<group>"; };
046C0D3F53B0B5EF0A1F5BEA /* RoomSummaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryTests.swift; sourceTree = "<group>"; };
048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
048A21188AB19349D026BECD /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
04BB8DDE245ED86C489BA983 /* AccessibilityIdentifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityIdentifiers.swift; sourceTree = "<group>"; };
04DF593C3F7AF4B2FBAEB05D /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
0516C69708D5CBDE1A8E77EC /* RoomDirectorySearchProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchProxyProtocol.swift; sourceTree = "<group>"; };
@@ -1534,7 +1535,7 @@
128501375217576AF0FE3E92 /* RoomAttachmentPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomAttachmentPicker.swift; sourceTree = "<group>"; };
12B09A94C519227264A41208 /* RoomMembershipDetailsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembershipDetailsProxy.swift; sourceTree = "<group>"; };
12FD5280AF55AB7F50F8E47D /* preview_avatar_room.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = preview_avatar_room.jpg; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = "<group>"; };
130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = "<group>"; };
136F80A613B55BDD071DCEA5 /* JoinRoomScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinRoomScreenModels.swift; sourceTree = "<group>"; };
13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -1642,7 +1643,7 @@
25F7FE40EF7490A7E09D7BE6 /* NotificationItemProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemProxy.swift; sourceTree = "<group>"; };
25F8664F1FB95AF3C4202478 /* PollFormScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenCoordinator.swift; sourceTree = "<group>"; };
260004737C573A56FA01E86E /* Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encodable.swift; sourceTree = "<group>"; };
267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; path = PreviewTests.xctestplan; sourceTree = "<group>"; };
267BB1D5B08A9511F894CB57 /* PreviewTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PreviewTests.xctestplan; sourceTree = "<group>"; };
26B0A96B8FE4849227945067 /* VoiceMessageRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecorder.swift; sourceTree = "<group>"; };
26EAAB54C6CE91D64B69A9F8 /* AppLockServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockServiceProtocol.swift; sourceTree = "<group>"; };
2711E5996016ABD6EAAEB58A /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
@@ -1723,7 +1724,7 @@
3558A15CFB934F9229301527 /* RestorationToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationToken.swift; sourceTree = "<group>"; };
35A057BA9BE0F079784CD061 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
35AFCF4C05DEED04E3DB1A16 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; };
36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxyMock.swift; sourceTree = "<group>"; };
37A63A59BFDDC494B1C20119 /* CallScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallScreenViewModel.swift; sourceTree = "<group>"; };
37CA26F55123E36B50DB0B3A /* AttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringTests.swift; sourceTree = "<group>"; };
@@ -2148,7 +2149,7 @@
8D55702474F279D910D2D162 /* RoomStateEventStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilder.swift; sourceTree = "<group>"; };
8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
8DA1E8F287680C8ED25EDBAC /* NetworkMonitorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkMonitorMock.swift; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = "<group>"; };
8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = "<group>"; };
8E1584F8BCF407BB94F48F04 /* EncryptionResetPasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreen.swift; sourceTree = "<group>"; };
8EAF4A49F3ACD8BB8B0D2371 /* ClientSDKMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSDKMock.swift; sourceTree = "<group>"; };
8F062DD2CCD95DC33528A16F /* KnockRequestProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnockRequestProxy.swift; sourceTree = "<group>"; };
@@ -2270,6 +2271,7 @@
A768CA51A59B8A5D8C8FD599 /* AuthenticationStartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreen.swift; sourceTree = "<group>"; };
A7978C9EFBDD7DE39BD86726 /* RestorationTokenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestorationTokenTests.swift; sourceTree = "<group>"; };
A7A1B80FE6E3BA72F9C748AD /* AdvancedSettingsScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsScreenViewModel.swift; sourceTree = "<group>"; };
A7AF83882E4388BB00ECD64C /* RoomRole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomRole.swift; sourceTree = "<group>"; };
A7C4EA55DA62F9D0F984A2AE /* CollapsibleTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleTimelineItem.swift; sourceTree = "<group>"; };
A7D452AF7B5F7E3A0A7DB54C /* SessionVerificationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
A7E37072597F67C4DD8CC2DB /* ComposerDraftServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerDraftServiceProtocol.swift; sourceTree = "<group>"; };
@@ -2290,7 +2292,7 @@
AAD8234D0E9C9B12BF9F240B /* LocationAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationAnnotation.swift; sourceTree = "<group>"; };
AB07F03461023BC39C730922 /* PhishingDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingDetector.swift; sourceTree = "<group>"; };
AB26D5444A4A7E095222DE8B /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; path = AccessibilityTests.xctestplan; sourceTree = "<group>"; };
AB389C38BD41EB3E47092CFB /* AccessibilityTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = AccessibilityTests.xctestplan; sourceTree = "<group>"; };
ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenViewModelProtocol.swift; sourceTree = "<group>"; };
AC0275CEE9CA078B34028BDF /* AppLockScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenViewModelTests.swift; sourceTree = "<group>"; };
AC1DA29A5A041CC0BACA7CB0 /* MockImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockImageCache.swift; sourceTree = "<group>"; };
@@ -2355,7 +2357,7 @@
B53AC78E49A297AC1D72A7CF /* AppMediator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMediator.swift; sourceTree = "<group>"; };
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = "<group>"; };
B5D829FD8958376614504B18 /* TargetConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetConfiguration.swift; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = "<group>"; };
B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = "<group>"; };
B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetails.swift; sourceTree = "<group>"; };
B65DDCF8E41759890355ACBC /* AuthenticationStartScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationStartScreenViewModelProtocol.swift; sourceTree = "<group>"; };
B68B31232312AFC844440BFE /* DeclineAndBlockScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeclineAndBlockScreenModels.swift; sourceTree = "<group>"; };
@@ -2383,7 +2385,7 @@
BA40B98B098B6F0371B750B3 /* TemplateScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenModels.swift; sourceTree = "<group>"; };
BA919F521E9F0EE3638AFC85 /* BugReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreen.swift; sourceTree = "<group>"; };
BB284643AF7AB131E307DCE0 /* AudioSessionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionProtocol.swift; sourceTree = "<group>"; };
BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; path = test_apple_image.heic; sourceTree = "<group>"; };
BB576F4118C35E6B5124FA22 /* test_apple_image.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_apple_image.heic; sourceTree = "<group>"; };
BB5B00A014307CE37B2812CD /* TimelineViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewModelProtocol.swift; sourceTree = "<group>"; };
BB6ED50FE104992419310EEB /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = "<group>"; };
BB8BC4C791D0E88CFCF4E5DF /* ServerSelectionScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -2481,7 +2483,7 @@
CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = "<group>"; };
CDE3F3911FF7CC639BDE5844 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
CEE20623EB4A9B88FB29F2BA /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/SAS.strings; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = "<group>"; };
CFFA5E881D281810AB428EA3 /* RoomPowerLevelsProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPowerLevelsProxy.swift; sourceTree = "<group>"; };
D01FD1171FF40E34D707FD00 /* BigIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigIcon.swift; sourceTree = "<group>"; };
D03D7ECAC68C2FFB8CF01BCB /* DeactivateAccountScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeactivateAccountScreen.swift; sourceTree = "<group>"; };
@@ -2545,7 +2547,7 @@
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = "<group>"; };
DC528B3764E3CF7FCFEF40E7 /* PollInteractionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollInteractionHandler.swift; sourceTree = "<group>"; };
DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; path = test_voice_message.m4a; sourceTree = "<group>"; };
DCA2D836BD10303F37FAAEED /* test_voice_message.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = test_voice_message.m4a; sourceTree = "<group>"; };
DCAC01A97A43BE07B9E94E43 /* ShareExtensionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareExtensionModels.swift; sourceTree = "<group>"; };
DCDAB580109C09A6AA97AF7E /* PollFormScreenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFormScreenTests.swift; sourceTree = "<group>"; };
DCF239C619971FDE48132550 /* SecureBackupLogoutConfirmationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupLogoutConfirmationScreenModels.swift; sourceTree = "<group>"; };
@@ -2591,7 +2593,7 @@
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
E53BFB7E4F329621C844E8C3 /* AnalyticsPromptScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreen.swift; sourceTree = "<group>"; };
E55B5EA766E89FF1F87C3ACB /* RoomNotificationSettingsProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomNotificationSettingsProxyProtocol.swift; sourceTree = "<group>"; };
E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; path = portrait_test_video.mp4; sourceTree = "<group>"; };
E5E7D4EE7CA295E5039FDA21 /* portrait_test_video.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = portrait_test_video.mp4; sourceTree = "<group>"; };
E5E94DCFEE803E5ABAE8ACCE /* KeychainControllerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerProtocol.swift; sourceTree = "<group>"; };
E5F2B6443D1ED8602F328539 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
E5FDFAA04174CC99FB66391C /* EditRoomAddressScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditRoomAddressScreenViewModel.swift; sourceTree = "<group>"; };
@@ -2635,7 +2637,7 @@
ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRoomTimelineView.swift; sourceTree = "<group>"; };
ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = "<group>"; };
ED33988DA4FD4FC666800106 /* SessionVerificationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenViewModel.swift; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = "<group>"; };
ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = "<group>"; };
ED49073BB1C1FC649DAC2CCD /* LocationRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationRoomTimelineView.swift; sourceTree = "<group>"; };
ED60E4D2CD678E1EBF16F77A /* BlockedUsersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreen.swift; sourceTree = "<group>"; };
EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = "<group>"; };
@@ -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;
@@ -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";
@@ -77,9 +77,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%1$d member</string>
<string>%1$d Member</string>
<key>other</key>
<string>%1$d members</string>
<string>%1$d Members</string>
</dict>
</dict>
<key>common_poll_votes_count</key>
@@ -77,9 +77,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%1$d membro</string>
<string>%1$d Membro</string>
<key>other</key>
<string>%1$d membri</string>
<string>%1$d Membri</string>
</dict>
</dict>
<key>common_poll_votes_count</key>
@@ -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 {
@@ -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 {
@@ -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:
@@ -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)
}
}
@@ -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,
+10
View File
@@ -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.
@@ -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 {
@@ -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)
}
}
@@ -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(())
@@ -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
]
}
@@ -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)
@@ -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
@@ -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)
}
}
@@ -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
@@ -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<RoomMemberDetails> = []
@@ -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.
@@ -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()
@@ -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)
}
}
@@ -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)
}
}
@@ -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()
@@ -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)
@@ -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 {
@@ -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))
@@ -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
}
@@ -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<RoomRolesAndPermissionsScreenViewModelAction, Never> = .init()
var actionsPublisher: AnyPublisher<RoomRolesAndPermissionsScreenViewModelAction, Never> {
@@ -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"
@@ -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<Image> {
if let administratorCount = context.viewState.administratorsAndOwnersCount {
.title("\(administratorCount)")
} else {
.isWaiting(true)
}
}
private var administratorDetails: ListRowDetails<Image> {
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")
}
}
@@ -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))
}
@@ -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
@@ -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
}
}
}
@@ -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 }
}
@@ -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) }
}
@@ -9,15 +9,15 @@ import Foundation
import MatrixRustSDK
struct RoomPermissionsSetting: Identifiable {
var id: KeyPath<RoomPermissions, RoomMemberDetails.Role> { keyPath }
var id: KeyPath<RoomPermissions, RoomRole> { 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<RoomPermissions, RoomMemberDetails.Role>
let keyPath: KeyPath<RoomPermissions, RoomRole>
/// The `RoomPowerLevelChanges` property that this setting is saved into.
var rustKeyPath: WritableKeyPath<RoomPowerLevelChanges, Int64?> {
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)
}
}
@@ -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
}
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df90344bb87a1b2f65e85cc65c749054dacac02c0c634501484bc02a7a9e6429
size 226433
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:caa50aaa716140d4613e6682fa1facb12cc57f6bf6ee8e29e801ac5a5ab03db9
size 238373
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9d40cb24ec76d87e4f8b0c4ead60a4fab50d8cc24ee0c903e527ddfaa5896b60
size 170864
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7639e4fa100e48262f35e996caa31cf9a67c0f10cb73a4be8c2fc64bf70ee8bf
size 180516
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:45bfc56242e51815e312057b7603d19c3f2ae488f81145f6e8c45bdd405bafd4
size 219778
oid sha256:356954ba65b0fe34441df8440182ab743df30aa9f6c6932bb9b5107ae87cb839
size 221488
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef779c260c378cb37d32e19aa9fb58c73818aca7153cdb762078487e6245b8cb
size 226361
oid sha256:7ce7e57ce7b90e91395dd3e0c9b3a73d944ac43a8f36feb77fb106e9f86add67
size 231973
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:08b1ed25327b18ce7be7831bf62644d25269b7204d79fb1058466893923f3559
size 172593
oid sha256:28dd538e435dc4125d34c8cbf45c5403a4dc1b4759d3d16cb0e56f4b1bb5a63f
size 167867
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:22561fa01da4cff09515ed5bf9c70beb55b2c691bf86c911aedc0090bef1396c
size 178152
oid sha256:d56fb49526da5e418e3311a8ab121efcde6cc5dd2234c3662997884e0f2e6991
size 176485
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06194fbeb1d19db9882525340b7a73d6cbae649c9a13f1b527e1d26306946c40
size 184357
oid sha256:5d7ee9d49212820a88171df680a31ad658ac62ae7fd3474a24403c130a4630d8
size 231534
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a14dd1411077dd6f20e46c67a8856169f04bc186bf7015ebe5fa37cdceb8981
size 195057
oid sha256:312f9f0d95ae3dd31c2c8ab4ce37a9de51febdd79a9d65b60205241e5a18f1cb
size 247092
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6382218fc0c357a9b4cabcbfb737e4678d2f52f89949a9ddab5301f3c70023fc
size 144983
oid sha256:ddc42a0c0de638440189c23ec7ef85c9b84ea2fdbc8e222994adbc5f4ab88d5a
size 172583
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1a2014f024ade0cb7901089e7637f7136474d057eb8dbb84577deecc7d3d16bb
size 154055
oid sha256:47fa5b57b70a18e75a9b690cc96e5bb3c7db26ccd06eae05f3d3e6f8bbd92647
size 188261
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dde88bcec2a8cc78858a4513f23a9b73aeab2478e52d730823e451e486e6d082
size 217524
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:37a45dbaf05d8dd1ad69d625aadcd9291efd5fecc490f267b72a60f903ec5986
size 224360
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a8e6022a3382c727169489bb965bf7ba2681417f4fb3123b4043745ca8887046
size 161674
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:484250de28a8f3804c89cf551cba3903675c6e2fd28ce80e4b25145a674b430b
size 168264
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:673f640cc1b7368d7c4bb70d5b5ac325941723cbf740507a0611606eee50d373
size 62443
oid sha256:60efb65986ae484b7f7da9648db252ab4aa11e0047020574244e070972813f0a
size 62712
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:673f640cc1b7368d7c4bb70d5b5ac325941723cbf740507a0611606eee50d373
size 62443
oid sha256:60efb65986ae484b7f7da9648db252ab4aa11e0047020574244e070972813f0a
size 62712
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d07e63d1d9dd2a676e05c95c3562e7e2ef397d9cb2ac07f8c5ca0c3eb510024
size 52234
oid sha256:e9f33cdbe4d65566be99949b00a54439b90d4d010ec9fb9af505fbc043271177
size 52462
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3d07e63d1d9dd2a676e05c95c3562e7e2ef397d9cb2ac07f8c5ca0c3eb510024
size 52234
oid sha256:e9f33cdbe4d65566be99949b00a54439b90d4d010ec9fb9af505fbc043271177
size 52462
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3c89484df5502aa6935e8c6babdcceb49b6701fbb4099437046bcffc22764ec1
size 129589
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:206708dbbb48e6f68d21b98103fd8600ca595395b0f3695a28560bf48a4a7ff8
size 139089
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:42f5682c78a9786db0ca354c284b369d666cec7136682e82edc01922d48a2d68
size 79075
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df0d7583c6493dd81930c92f23c74e60467743a94bada42ecf6fb17121ccb117
size 100746
+6 -13
View File
@@ -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
@@ -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,
@@ -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]) {