Room's details edit screen (#956)

* Add RoomDetailsEditScreen template

* Add navigation to the edit screen

* Delete template code

* Start RoomDetailsEditScreen UI

* Manage power levels in RoomDetailsScreen

* Start RoomDetailsEditScreenViewModel logic

* Inject initial room state

* Add cancel action

* Expose set name/topic APIs in RoomProxy

* Refine RoomDetailsEditScreen UI

* Add save logic

* Add localisations

* Fix avatar image

* Update localisations

* Add “Add topic” button

* Add feature flag

* Add dismiss on save logic

* Reduce throttling

* Improve form logic

* Fix UT build errors

* Add media sheet

* Add media preprocessing

* Add LoadableEditableAvatarImage

* Add condition on delete image

* Add avatar save button logic

* Add remove avatar logic

* Cleanup

* Fix edit bug in DM

* Add upload avatar

* Add focus

* Add RoomDetailsViewModel UTs

* Fix button style

* Add UTs

* Add empty topic ui test

* Fix iPad sheet presentation

* Revert topic appearance in room’s details

* Address PR comments

* Add UI tests
This commit is contained in:
Alfonso Grillo
2023-05-26 15:47:12 +02:00
committed by GitHub
parent 524997438f
commit f2b7faa183
47 changed files with 1281 additions and 54 deletions
+48
View File
@@ -32,6 +32,7 @@
09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; }; 09C83DDDB07C28364F325209 /* MockRoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */; };
0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */; }; 0AA0477E063E72B786A983CF /* AnalyticsPromptScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */; };
0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; }; 0AE0AB1952F186EB86719B4F /* HomeScreenRoomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */; };
0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */; };
0BE4D5CBF86956410F071F91 /* CreateRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */; }; 0BE4D5CBF86956410F071F91 /* CreateRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */; };
0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; }; 0BFA67AFD757EE2BA569836A /* ScrollViewAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53482ECA4B6633961EC224F5 /* ScrollViewAdapter.swift */; };
0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */; }; 0C47AE2CA7929CB3B0E2D793 /* ServerSelectionScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */; };
@@ -232,6 +233,7 @@
651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; }; 651341E67C3514F9811A1EC1 /* LoginScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */; };
652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; }; 652ACCF104A8CEF30788963C /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1423AB065857FA546444DB15 /* NotificationManager.swift */; };
6530865EB9A8C0F0AF0216DA /* ServerSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */; }; 6530865EB9A8C0F0AF0216DA /* ServerSelectionScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9501D11B4258DFA33BA3B40F /* ServerSelectionScreenModels.swift */; };
659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; };
65EDA77363BEDC40CDE43B43 /* InvitesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ADEA322D2089391E049535 /* InvitesScreen.swift */; }; 65EDA77363BEDC40CDE43B43 /* InvitesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ADEA322D2089391E049535 /* InvitesScreen.swift */; };
661A664C6EDF856B05519206 /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F562E2CBA002E8E1B6545C38 /* FilePreviewScreen.swift */; }; 661A664C6EDF856B05519206 /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F562E2CBA002E8E1B6545C38 /* FilePreviewScreen.swift */; };
663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; };
@@ -297,6 +299,7 @@
7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; }; 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB8E75B9CB6C78BE8D09B1AF /* OnboardingScreen.swift */; };
7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */; }; 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */; };
8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; }; 8024BE37156FF0A95A7A3465 /* AnalyticsPromptUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF11DD57D9FACF2A757AB024 /* AnalyticsPromptUITests.swift */; };
804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */; };
80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; }; 80D00A7C62AAB44F54725C43 /* PermalinkBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F754E66A8970963B15B2A41E /* PermalinkBuilder.swift */; };
80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */; }; 80DEA2A4B20F9E279EAE6B2B /* UserProfile+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD01F7FC2BBAC7351948595 /* UserProfile+Mock.swift */; };
8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; }; 8196A2E71ACC902DD69F24EE /* UserNotificationControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE6C5C756E1393202BA95CD /* UserNotificationControllerTests.swift */; };
@@ -386,6 +389,7 @@
9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; }; 9D9690D2FD4CD26FF670620F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75EF87651B00A176AB08E97 /* AppDelegate.swift */; };
9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; }; 9DC5FB22B8F86C3B51E907C1 /* HomeScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6E4C37E9F0E53D3DF951AC /* HomeScreenUITests.swift */; };
9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; }; 9DD5AA10E85137140FEA86A3 /* MediaProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17EFA1D3D09FC2F9C5E1CB2 /* MediaProvider.swift */; };
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */; };
9DF3F6318A4402305F5EB869 /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */; }; 9DF3F6318A4402305F5EB869 /* AnalyticsPromptScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F8002D0392A476D2758B291 /* AnalyticsPromptScreen.swift */; };
9E838A62918E47BC72D6640D /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */; }; 9E838A62918E47BC72D6640D /* UserIndicatorPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB54B4F94686CCF0289B72F /* UserIndicatorPresenter.swift */; };
9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */; }; 9F19096BFA629C0AC282B1E4 /* CreateRoomScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CEB4634C0DD7779C4AB504 /* CreateRoomScreenUITests.swift */; };
@@ -411,6 +415,7 @@
A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; }; A50849766F056FD1DB942DEA /* AlertInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EEB64CC6F3DF5B68736A6B4 /* AlertInfo.swift */; };
A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; }; A5B9EF45C7B8ACEB4954AE36 /* LoginScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9780389F8A53E4D26E23DD03 /* LoginScreenViewModelProtocol.swift */; };
A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; }; A5D551E5691749066E0E0C44 /* RoomDetailsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 837B440C4705E4B899BCB899 /* RoomDetailsScreenViewModel.swift */; };
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */; };
A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; }; A6DEC1ADEC8FEEC206A0FA37 /* AttributedStringBuilderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72F37B5DA798C9AE436F2C2C /* AttributedStringBuilderProtocol.swift */; };
A6F713461DB62AC06293E7B7 /* FilePreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820637A0F9C2F562FF40CBC8 /* FilePreviewScreenModels.swift */; }; A6F713461DB62AC06293E7B7 /* FilePreviewScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 820637A0F9C2F562FF40CBC8 /* FilePreviewScreenModels.swift */; };
A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; }; A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A18F6CE4D694D21E4EA9B25 /* Strings+Untranslated.swift */; };
@@ -490,6 +495,7 @@
C4078364FD9FA00EA9D00A15 /* RoomMembersListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */; }; C4078364FD9FA00EA9D00A15 /* RoomMembersListScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */; };
C413D36D44F89DE63D3ADFA4 /* ReportContentScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A433BE28B40D418237BE37B5 /* ReportContentScreen.swift */; }; C413D36D44F89DE63D3ADFA4 /* ReportContentScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = A433BE28B40D418237BE37B5 /* ReportContentScreen.swift */; };
C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; }; C4180F418235DAD9DD173951 /* TemplateScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9873076F224E4CE09D8BD47D /* TemplateScreenUITests.swift */; };
C49FCC766673006B6D299F1C /* RoomDetailsEditScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */; };
C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */; }; C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2E9B841EE4878283ECDB554 /* InviteUsersScreen.swift */; };
C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */; }; C4E0D03DF88242697545A9B7 /* UserIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1275D9CE0FFBA6E8E85426 /* UserIndicatorController.swift */; };
C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; }; C4F69156C31A447FEFF2A47C /* DTHTMLElement+AttributedStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */; };
@@ -570,6 +576,7 @@
E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; }; E67418DACEDBC29E988E6ACD /* message.caf in Resources */ = {isa = PBXBuildFile; fileRef = ED482057AE39D5C6D9C5F3D8 /* message.caf */; };
E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */; }; E75CE800B3E64D0F7F8E228D /* TemplateScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */; };
E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */; }; E77469C5CD7F7F58C0AC9752 /* test_pdf.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 3FFDA99C98BE05F43A92343B /* test_pdf.pdf */; };
E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */; };
E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; }; E794AB6ABE1FF5AF0573FEA1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332DFE9642F0A46ECA0497B /* BlurHashEncode.swift */; };
E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; }; E89536FC8C0E4B79E9842A78 /* RoomTimelineControllerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0197EAE9D45A662B8847B6 /* RoomTimelineControllerProtocol.swift */; };
E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; }; E9560744F7B0292E20ECE5F2 /* RoomDetailsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */; };
@@ -596,6 +603,7 @@
F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; }; F07D88421A9BC4D03D4A5055 /* VideoRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F348B5F2C12F9D4F4B4D3884 /* VideoRoomTimelineItem.swift */; };
F0F82C3C848C865C3098AA52 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; }; F0F82C3C848C865C3098AA52 /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = 21C83087604B154AA30E9A8F /* SnapshotTesting */; };
F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; }; F118DD449066E594F63C697D /* RoomMemberProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32B5E17028C02DFA7DDA3931 /* RoomMemberProxyProtocol.swift */; };
F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */; };
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; }; F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; };
F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; }; F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */; }; F257F964493A9CD02A6F720C /* OnboardingPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */; };
@@ -681,6 +689,7 @@
00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; }; 00245D40CD90FD71D6A05239 /* EmojiPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreen.swift; sourceTree = "<group>"; };
00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = "<group>"; }; 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelTests.swift; sourceTree = "<group>"; };
00B62EE933FC3D5651AF4607 /* TimelineEventProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventProxy.swift; sourceTree = "<group>"; }; 00B62EE933FC3D5651AF4607 /* TimelineEventProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventProxy.swift; sourceTree = "<group>"; };
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelTests.swift; sourceTree = "<group>"; };
01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 01C4C7DB37597D7D8379511A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
024F7398C5FC12586FB10E9D /* EffectsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsScene.swift; sourceTree = "<group>"; }; 024F7398C5FC12586FB10E9D /* EffectsScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsScene.swift; sourceTree = "<group>"; };
0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceTest.swift; sourceTree = "<group>"; }; 0287793F11C480E242B03DF5 /* UserDiscoveryServiceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoveryServiceTest.swift; sourceTree = "<group>"; };
@@ -704,6 +713,7 @@
095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = "<group>"; }; 095AED4CF56DFF3EB7BB84C8 /* RoomTimelineProviderProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineProviderProtocol.swift; sourceTree = "<group>"; };
0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = "<group>"; }; 0960A7F5C1B0B6679BDF26F9 /* ElementToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToggleStyle.swift; sourceTree = "<group>"; };
099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; }; 099F2D36C141D845A445B1E6 /* EmojiProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiProviderTests.swift; sourceTree = "<group>"; };
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenCoordinator.swift; sourceTree = "<group>"; };
0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; }; 0B987FC3FDBAA0E1C5AA235C /* PaginationIndicatorRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationIndicatorRoomTimelineItem.swift; sourceTree = "<group>"; };
0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = "<group>"; }; 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSenderAvatarView.swift; sourceTree = "<group>"; };
0C671107BDFC6CD1778C0B4C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 0C671107BDFC6CD1778C0B4C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -723,6 +733,7 @@
111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; }; 111B698739E3410E2CDB7144 /* MXLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MXLog.swift; sourceTree = "<group>"; };
11F7F3CF7E70518BD7D25E04 /* EmojiMartEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartEmoji.swift; sourceTree = "<group>"; }; 11F7F3CF7E70518BD7D25E04 /* EmojiMartEmoji.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiMartEmoji.swift; sourceTree = "<group>"; };
1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 1215A4FC53D2319E81AE8970 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenUITests.swift; sourceTree = "<group>"; };
1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; }; 1222DB76B917EB8A55365BA5 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = "<group>"; };
127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = "<group>"; }; 127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = "<group>"; };
127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; }; 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = "<group>"; };
@@ -735,6 +746,7 @@
1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = "<group>"; }; 1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = "<group>"; };
15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = "<group>"; }; 15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = "<group>"; };
16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenUITests.swift; sourceTree = "<group>"; }; 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenUITests.swift; sourceTree = "<group>"; };
16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = "<group>"; };
16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; }; 16DC8C5B2991724903F1FA6A /* AppIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = AppIcon.pdf; sourceTree = "<group>"; };
1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = "<group>"; }; 1715E3D7F53C0748AA50C91C /* PostHogAnalyticsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogAnalyticsClient.swift; sourceTree = "<group>"; };
1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = "<group>"; }; 1734A445A58ED855B977A0A8 /* TracingConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfigurationTests.swift; sourceTree = "<group>"; };
@@ -754,6 +766,7 @@
1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = "<group>"; }; 1D56469A9EE0CFA2B7BA9760 /* SessionVerificationControllerProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationControllerProxyProtocol.swift; sourceTree = "<group>"; };
1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = "<group>"; }; 1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenUITests.swift; sourceTree = "<group>"; };
1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageView.swift; sourceTree = "<group>"; }; 1DF2717AB91060260E5F4781 /* OnboardingPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageView.swift; sourceTree = "<group>"; };
1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModelProtocol.swift; sourceTree = "<group>"; };
1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = "<group>"; }; 1E508AB0EDEE017FF4F6F8D1 /* DTHTMLElement+AttributedStringBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DTHTMLElement+AttributedStringBuilder.swift"; sourceTree = "<group>"; };
1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = "<group>"; }; 1F2529D434C750ED78ADF1ED /* UserAgentBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentBuilder.swift; sourceTree = "<group>"; };
1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItemContent.swift; sourceTree = "<group>"; }; 1FD51B4D5173F7FC886F5360 /* NoticeRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineItemContent.swift; sourceTree = "<group>"; };
@@ -894,6 +907,7 @@
6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = "<group>"; };
63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModel.swift; sourceTree = "<group>"; }; 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModel.swift; sourceTree = "<group>"; };
63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = "<group>"; }; 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverridableAvatarImage.swift; sourceTree = "<group>"; };
6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = "<group>"; }; 6493AC9979CEB1410302BFE3 /* RoomDetailsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenCoordinator.swift; sourceTree = "<group>"; };
64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = "<group>"; }; 64F49FB9EE2913234F06CE68 /* MediaPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerScreenCoordinator.swift; sourceTree = "<group>"; };
653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 653610CB5F9776EAAAB98155 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -1165,6 +1179,7 @@
D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = "<group>"; }; D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = "<group>"; };
D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = "<group>"; }; D8F5F9E02B1AB5350B1815E7 /* TimelineStartRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStartRoomTimelineItem.swift; sourceTree = "<group>"; };
DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; }; DA2AEC1AB349A341FE13DEC1 /* StartChatScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartChatScreenUITests.swift; sourceTree = "<group>"; };
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreen.swift; sourceTree = "<group>"; };
DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsKeys.swift; sourceTree = "<group>"; }; DBA8DC95C079805B0B56E8A9 /* SharedUserDefaultsKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUserDefaultsKeys.swift; sourceTree = "<group>"; };
DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; }; DBFEAC3AC691CBB84983E275 /* ElementXTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXTests.swift; sourceTree = "<group>"; };
DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; }; DC0AEA686E425F86F6BA0404 /* UNNotification+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Creator.swift"; sourceTree = "<group>"; };
@@ -1183,6 +1198,7 @@
E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; }; E36CB905A2B9EC2C92A2DA7C /* KeychainController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainController.swift; sourceTree = "<group>"; };
E39CCFA7537FAD50386FDA00 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; }; E39CCFA7537FAD50386FDA00 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; }; E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsClientProtocol.swift; sourceTree = "<group>"; };
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenViewModel.swift; sourceTree = "<group>"; };
E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = "<group>"; }; E43005941B3A2C9671E23C85 /* UserIndicatorModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorModalView.swift; sourceTree = "<group>"; };
E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; }; E51E3D86A84341C3A0CB8A40 /* FileRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileRoomTimelineView.swift; sourceTree = "<group>"; };
E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; }; E5272BC4A60B6AD7553BACA1 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
@@ -1610,6 +1626,7 @@
B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */, B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */,
C352359663A0E52BA20761EE /* LoadableImage.swift */, C352359663A0E52BA20761EE /* LoadableImage.swift */,
50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */, 50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */,
648DD1C10E4957CB791FE0B8 /* OverridableAvatarImage.swift */,
C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */, C705E605EF57C19DBE86FFA1 /* PlaceholderAvatarImage.swift */,
839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */, 839E2C35DF3F9C7B54C3CE49 /* RoundedCornerShape.swift */,
923485F85E1D765EF9D20E88 /* UserProfileCell.swift */, 923485F85E1D765EF9D20E88 /* UserProfileCell.swift */,
@@ -2004,6 +2021,14 @@
path = View; path = View;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
5F6CB68B44F6C587E463A934 /* View */ = {
isa = PBXGroup;
children = (
DB06F22CFA34885B40976061 /* RoomDetailsEditScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
605F8221E52991786397FCC9 /* View */ = { 605F8221E52991786397FCC9 /* View */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -2176,6 +2201,7 @@
00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */, 00A941F289F6AB876BA3361A /* OnboardingViewModelTests.swift */,
6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */, 6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */,
086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */, 086C19086DD16E9B38E25954 /* ReportContentViewModelTests.swift */,
00E5B2CBEF8F96424F095508 /* RoomDetailsEditScreenViewModelTests.swift */,
2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */, 2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */,
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */, EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */,
69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */, 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */,
@@ -2491,6 +2517,7 @@
39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */, 39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */,
0C88046D6A070D9827181C4D /* OnboardingUITests.swift */, 0C88046D6A070D9827181C4D /* OnboardingUITests.swift */,
4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */, 4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */,
122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */,
3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */, 3BFDAF6918BB096C44788FC9 /* RoomDetailsScreenUITests.swift */,
0F19DBE940499D3E3DD405D8 /* RoomMemberDetailsScreenUITests.swift */, 0F19DBE940499D3E3DD405D8 /* RoomMemberDetailsScreenUITests.swift */,
C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */, C5B7A755E985FA14469E86B2 /* RoomMembersListScreenUITests.swift */,
@@ -2940,6 +2967,7 @@
3F38EAC92E2281990E65DAF2 /* OnboardingScreen */, 3F38EAC92E2281990E65DAF2 /* OnboardingScreen */,
A448A3A8F764174C60CD0CA1 /* Other */, A448A3A8F764174C60CD0CA1 /* Other */,
5970F275D6014548DCED6106 /* ReportContentScreen */, 5970F275D6014548DCED6106 /* ReportContentScreen */,
E71742A824A7192C8D378875 /* RoomDetailsEditScreen */,
E703BBD16266053B8A193C7B /* RoomDetailsScreen */, E703BBD16266053B8A193C7B /* RoomDetailsScreen */,
B86CF59E083C82C2A842E4AD /* RoomMemberDetailsScreen */, B86CF59E083C82C2A842E4AD /* RoomMemberDetailsScreen */,
D4DB8163C10389C069458252 /* RoomMemberListScreen */, D4DB8163C10389C069458252 /* RoomMemberListScreen */,
@@ -2986,6 +3014,18 @@
path = RoomDetailsScreen; path = RoomDetailsScreen;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
E71742A824A7192C8D378875 /* RoomDetailsEditScreen */ = {
isa = PBXGroup;
children = (
0A3E77399BD262D301451BF2 /* RoomDetailsEditScreenCoordinator.swift */,
16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */,
E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */,
1DF8F7A3AD83D04C08D75E01 /* RoomDetailsEditScreenViewModelProtocol.swift */,
5F6CB68B44F6C587E463A934 /* View */,
);
path = RoomDetailsEditScreen;
sourceTree = "<group>";
};
E74CD7681375AD2EAA34D66B /* Authentication */ = { E74CD7681375AD2EAA34D66B /* Authentication */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -3559,6 +3599,7 @@
27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */, 27E9263DA75E266690A37EB1 /* PermalinkBuilderTests.swift in Sources */,
D415764645491F10344FC6AC /* Publisher.swift in Sources */, D415764645491F10344FC6AC /* Publisher.swift in Sources */,
D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */, D53B80EF02C1062E68659EDD /* ReportContentViewModelTests.swift in Sources */,
9DD84E014ADFB2DD813022D5 /* RoomDetailsEditScreenViewModelTests.swift in Sources */,
EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */, EA974337FA7D040E7C74FE6E /* RoomDetailsViewModelTests.swift in Sources */,
6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */, 6B31508C6334C617360C2EAB /* RoomMemberDetailsViewModelTests.swift in Sources */,
CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */, CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */,
@@ -3820,6 +3861,7 @@
7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */, 7F64FA937B95924B3A44EC12 /* OnboardingScreen.swift in Sources */,
CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */, CE7148E80F09B7305E026AC6 /* OnboardingViewModel.swift in Sources */,
992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */, 992477AB8E3F3C36D627D32E /* OnboardingViewModelProtocol.swift in Sources */,
804C15D8ADE0EA7A5268F58A /* OverridableAvatarImage.swift in Sources */,
CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */, CD6A72B65D3B6076F4045C30 /* PHGPostHogConfiguration.swift in Sources */,
7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */, 7501442D52A65F73DF79FFD4 /* PaginationIndicatorRoomTimelineItem.swift in Sources */,
764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */, 764AFCC225B044CF5F9B41E5 /* PaginationIndicatorRoomTimelineView.swift in Sources */,
@@ -3841,6 +3883,11 @@
8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */, 8285FF4B2C2331758C437FF7 /* ReportContentScreenViewModelProtocol.swift in Sources */,
A494741843F087881299ACF0 /* RestorationToken.swift in Sources */, A494741843F087881299ACF0 /* RestorationToken.swift in Sources */,
755EE5B0998C6A4D764D86E5 /* RoomAttachmentPicker.swift in Sources */, 755EE5B0998C6A4D764D86E5 /* RoomAttachmentPicker.swift in Sources */,
0BDA19079FD6E17C5AC62E22 /* RoomDetailsEditScreen.swift in Sources */,
E78D429F18071545BF661A52 /* RoomDetailsEditScreenCoordinator.swift in Sources */,
A6D4C5EEA85A6A0ABA1559D6 /* RoomDetailsEditScreenModels.swift in Sources */,
659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */,
C49FCC766673006B6D299F1C /* RoomDetailsEditScreenViewModelProtocol.swift in Sources */,
126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */, 126EE01D8BEAEF26105D83C5 /* RoomDetailsScreen.swift in Sources */,
FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */, FA5A7E32B1920FCB4EEDC1BA /* RoomDetailsScreenCoordinator.swift in Sources */,
DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */, DB079D1929B5A5F52D207C83 /* RoomDetailsScreenModels.swift in Sources */,
@@ -4032,6 +4079,7 @@
7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */, 7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */,
6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */, 6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */,
BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */, BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */,
F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */,
829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */, 829062DD3C3F7016FE1A6476 /* RoomDetailsScreenUITests.swift in Sources */,
A8771F5975A82759FA5138AE /* RoomMemberDetailsScreenUITests.swift in Sources */, A8771F5975A82759FA5138AE /* RoomMemberDetailsScreenUITests.swift in Sources */,
44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */, 44121202B4A260C98BF615A7 /* RoomMembersListScreenUITests.swift in Sources */,
@@ -69,6 +69,7 @@
"common_file" = "File"; "common_file" = "File";
"common_gif" = "GIF"; "common_gif" = "GIF";
"common_image" = "Image"; "common_image" = "Image";
"common_invite_unknown_profile" = "We can’t validate this user’s Matrix ID. The invite might not be received.";
"common_leaving_room" = "Leaving room"; "common_leaving_room" = "Leaving room";
"common_link_copied_to_clipboard" = "Link copied to clipboard"; "common_link_copied_to_clipboard" = "Link copied to clipboard";
"common_loading" = "Loading…"; "common_loading" = "Loading…";
@@ -85,6 +86,7 @@
"common_replying_to" = "Replying to %1$@"; "common_replying_to" = "Replying to %1$@";
"common_report_a_bug" = "Report a bug"; "common_report_a_bug" = "Report a bug";
"common_report_submitted" = "Report submitted"; "common_report_submitted" = "Report submitted";
"common_room_name" = "Room name";
"common_search_for_someone" = "Search for someone"; "common_search_for_someone" = "Search for someone";
"common_search_results" = "Search results"; "common_search_results" = "Search results";
"common_security" = "Security"; "common_security" = "Security";
@@ -224,9 +226,12 @@
"screen_room_attachment_source_camera_video" = "Record a video"; "screen_room_attachment_source_camera_video" = "Record a video";
"screen_room_attachment_source_files" = "Attachment"; "screen_room_attachment_source_files" = "Attachment";
"screen_room_attachment_source_gallery" = "Photo & Video Library"; "screen_room_attachment_source_gallery" = "Photo & Video Library";
"screen_room_details_add_topic_title" = "Add topic";
"screen_room_details_already_a_member" = "Already a member"; "screen_room_details_already_a_member" = "Already a member";
"screen_room_details_already_invited" = "Already invited"; "screen_room_details_already_invited" = "Already invited";
"screen_room_details_edition_error" = "An error occurred when updating the room details"; "screen_room_details_edit_room_title" = "Edit Room";
"screen_room_details_edition_error" = "We were unable to update all the information for this room.";
"screen_room_details_edition_error_title" = "Unable to update room";
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."; "screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
"screen_room_details_encryption_enabled_title" = "Message encryption enabled"; "screen_room_details_encryption_enabled_title" = "Message encryption enabled";
"screen_room_details_share_room_title" = "Share room"; "screen_room_details_share_room_title" = "Share room";
@@ -259,7 +264,6 @@
"screen_signout_confirmation_dialog_title" = "Sign out"; "screen_signout_confirmation_dialog_title" = "Sign out";
"screen_signout_in_progress_dialog_content" = "Signing out…"; "screen_signout_in_progress_dialog_content" = "Signing out…";
"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat"; "screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat";
"screen_start_chat_unknown_profile" = "We can’t validate this user’s Matrix ID. The invite might not be received.";
"session_verification_banner_message" = "Looks like you’re using a new device. Verify it’s you to access your encrypted messages."; "session_verification_banner_message" = "Looks like you’re using a new device. Verify it’s you to access your encrypted messages.";
"session_verification_banner_title" = "Access your message history"; "session_verification_banner_title" = "Access your message history";
"settings_rageshake" = "Rageshake"; "settings_rageshake" = "Rageshake";
@@ -29,6 +29,7 @@ final class AppSettings {
case shouldCollapseRoomStateEvents case shouldCollapseRoomStateEvents
case startChatFlowEnabled case startChatFlowEnabled
case startChatUserSuggestionsEnabled case startChatUserSuggestionsEnabled
case editRoomDetailsFlowEnabled
case invitesFlowEnabled case invitesFlowEnabled
case inviteMorePeopleFlowEnabled case inviteMorePeopleFlowEnabled
case readReceiptsEnabled case readReceiptsEnabled
@@ -196,4 +197,9 @@ final class AppSettings {
@UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: false, storageType: .userDefaults(store)) @UserPreference(key: UserDefaultsKeys.readReceiptsEnabled, defaultValue: false, storageType: .userDefaults(store))
var readReceiptsEnabled var readReceiptsEnabled
// MARK: Room details edit
@UserPreference(key: UserDefaultsKeys.editRoomDetailsFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
var editRoomDetailsFlowEnabled
} }
+11 -3
View File
@@ -154,6 +154,8 @@ public enum L10n {
public static var commonGif: String { return L10n.tr("Localizable", "common_gif") } public static var commonGif: String { return L10n.tr("Localizable", "common_gif") }
/// Image /// Image
public static var commonImage: String { return L10n.tr("Localizable", "common_image") } public static var commonImage: String { return L10n.tr("Localizable", "common_image") }
/// We can’t validate this user’s Matrix ID. The invite might not be received.
public static var commonInviteUnknownProfile: String { return L10n.tr("Localizable", "common_invite_unknown_profile") }
/// Leaving room /// Leaving room
public static var commonLeavingRoom: String { return L10n.tr("Localizable", "common_leaving_room") } public static var commonLeavingRoom: String { return L10n.tr("Localizable", "common_leaving_room") }
/// Link copied to clipboard /// Link copied to clipboard
@@ -192,6 +194,8 @@ public enum L10n {
public static var commonReportABug: String { return L10n.tr("Localizable", "common_report_a_bug") } public static var commonReportABug: String { return L10n.tr("Localizable", "common_report_a_bug") }
/// Report submitted /// Report submitted
public static var commonReportSubmitted: String { return L10n.tr("Localizable", "common_report_submitted") } public static var commonReportSubmitted: String { return L10n.tr("Localizable", "common_report_submitted") }
/// Room name
public static var commonRoomName: String { return L10n.tr("Localizable", "common_room_name") }
/// Search for someone /// Search for someone
public static var commonSearchForSomeone: String { return L10n.tr("Localizable", "common_search_for_someone") } public static var commonSearchForSomeone: String { return L10n.tr("Localizable", "common_search_for_someone") }
/// Search results /// Search results
@@ -586,12 +590,18 @@ public enum L10n {
public static var screenRoomAttachmentSourceFiles: String { return L10n.tr("Localizable", "screen_room_attachment_source_files") } public static var screenRoomAttachmentSourceFiles: String { return L10n.tr("Localizable", "screen_room_attachment_source_files") }
/// Photo & Video Library /// Photo & Video Library
public static var screenRoomAttachmentSourceGallery: String { return L10n.tr("Localizable", "screen_room_attachment_source_gallery") } public static var screenRoomAttachmentSourceGallery: String { return L10n.tr("Localizable", "screen_room_attachment_source_gallery") }
/// Add topic
public static var screenRoomDetailsAddTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_add_topic_title") }
/// Already a member /// Already a member
public static var screenRoomDetailsAlreadyAMember: String { return L10n.tr("Localizable", "screen_room_details_already_a_member") } public static var screenRoomDetailsAlreadyAMember: String { return L10n.tr("Localizable", "screen_room_details_already_a_member") }
/// Already invited /// Already invited
public static var screenRoomDetailsAlreadyInvited: String { return L10n.tr("Localizable", "screen_room_details_already_invited") } public static var screenRoomDetailsAlreadyInvited: String { return L10n.tr("Localizable", "screen_room_details_already_invited") }
/// An error occurred when updating the room details /// Edit Room
public static var screenRoomDetailsEditRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_edit_room_title") }
/// We were unable to update all the information for this room.
public static var screenRoomDetailsEditionError: String { return L10n.tr("Localizable", "screen_room_details_edition_error") } public static var screenRoomDetailsEditionError: String { return L10n.tr("Localizable", "screen_room_details_edition_error") }
/// Unable to update room
public static var screenRoomDetailsEditionErrorTitle: String { return L10n.tr("Localizable", "screen_room_details_edition_error_title") }
/// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them. /// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.
public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") } public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") }
/// Message encryption enabled /// Message encryption enabled
@@ -680,8 +690,6 @@ public enum L10n {
public static var screenSignoutPreferenceItem: String { return L10n.tr("Localizable", "screen_signout_preference_item") } public static var screenSignoutPreferenceItem: String { return L10n.tr("Localizable", "screen_signout_preference_item") }
/// An error occurred when trying to start a chat /// An error occurred when trying to start a chat
public static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") } public static var screenStartChatErrorStartingChat: String { return L10n.tr("Localizable", "screen_start_chat_error_starting_chat") }
/// We can’t validate this user’s Matrix ID. The invite might not be received.
public static var screenStartChatUnknownProfile: String { return L10n.tr("Localizable", "screen_start_chat_unknown_profile") }
/// Looks like you’re using a new device. Verify it’s you to access your encrypted messages. /// Looks like you’re using a new device. Verify it’s you to access your encrypted messages.
public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") } public static var sessionVerificationBannerMessage: String { return L10n.tr("Localizable", "session_verification_banner_message") }
/// Access your message history /// Access your message history
@@ -371,6 +371,27 @@ class RoomMemberProxyMock: RoomMemberProxyProtocol {
return unignoreUserReturnValue return unignoreUserReturnValue
} }
} }
//MARK: - canSendStateEvent
var canSendStateEventTypeCallsCount = 0
var canSendStateEventTypeCalled: Bool {
return canSendStateEventTypeCallsCount > 0
}
var canSendStateEventTypeReceivedType: StateEventType?
var canSendStateEventTypeReceivedInvocations: [StateEventType] = []
var canSendStateEventTypeReturnValue: Bool!
var canSendStateEventTypeClosure: ((StateEventType) -> Bool)?
func canSendStateEvent(type: StateEventType) -> Bool {
canSendStateEventTypeCallsCount += 1
canSendStateEventTypeReceivedType = type
canSendStateEventTypeReceivedInvocations.append(type)
if let canSendStateEventTypeClosure = canSendStateEventTypeClosure {
return canSendStateEventTypeClosure(type)
} else {
return canSendStateEventTypeReturnValue
}
}
} }
class RoomProxyMock: RoomProxyProtocol { class RoomProxyMock: RoomProxyProtocol {
var id: String { var id: String {
@@ -419,11 +440,6 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingMembersPublisher = value } set(value) { underlyingMembersPublisher = value }
} }
var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>! var underlyingMembersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never>!
var updatesPublisher: AnyPublisher<TimelineDiff, Never> {
get { return underlyingUpdatesPublisher }
set(value) { underlyingUpdatesPublisher = value }
}
var underlyingUpdatesPublisher: AnyPublisher<TimelineDiff, Never>!
var invitedMembersCount: UInt { var invitedMembersCount: UInt {
get { return underlyingInvitedMembersCount } get { return underlyingInvitedMembersCount }
set(value) { underlyingInvitedMembersCount = value } set(value) { underlyingInvitedMembersCount = value }
@@ -439,6 +455,11 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingActiveMembersCount = value } set(value) { underlyingActiveMembersCount = value }
} }
var underlyingActiveMembersCount: UInt! var underlyingActiveMembersCount: UInt!
var updatesPublisher: AnyPublisher<TimelineDiff, Never> {
get { return underlyingUpdatesPublisher }
set(value) { underlyingUpdatesPublisher = value }
}
var underlyingUpdatesPublisher: AnyPublisher<TimelineDiff, Never>!
//MARK: - loadAvatarURLForUserId //MARK: - loadAvatarURLForUserId
@@ -884,6 +905,86 @@ class RoomProxyMock: RoomProxyProtocol {
return inviteUserIDReturnValue return inviteUserIDReturnValue
} }
} }
//MARK: - setName
var setNameCallsCount = 0
var setNameCalled: Bool {
return setNameCallsCount > 0
}
var setNameReceivedName: String?
var setNameReceivedInvocations: [String?] = []
var setNameReturnValue: Result<Void, RoomProxyError>!
var setNameClosure: ((String?) async -> Result<Void, RoomProxyError>)?
func setName(_ name: String?) async -> Result<Void, RoomProxyError> {
setNameCallsCount += 1
setNameReceivedName = name
setNameReceivedInvocations.append(name)
if let setNameClosure = setNameClosure {
return await setNameClosure(name)
} else {
return setNameReturnValue
}
}
//MARK: - setTopic
var setTopicCallsCount = 0
var setTopicCalled: Bool {
return setTopicCallsCount > 0
}
var setTopicReceivedTopic: String?
var setTopicReceivedInvocations: [String] = []
var setTopicReturnValue: Result<Void, RoomProxyError>!
var setTopicClosure: ((String) async -> Result<Void, RoomProxyError>)?
func setTopic(_ topic: String) async -> Result<Void, RoomProxyError> {
setTopicCallsCount += 1
setTopicReceivedTopic = topic
setTopicReceivedInvocations.append(topic)
if let setTopicClosure = setTopicClosure {
return await setTopicClosure(topic)
} else {
return setTopicReturnValue
}
}
//MARK: - removeAvatar
var removeAvatarCallsCount = 0
var removeAvatarCalled: Bool {
return removeAvatarCallsCount > 0
}
var removeAvatarReturnValue: Result<Void, RoomProxyError>!
var removeAvatarClosure: (() async -> Result<Void, RoomProxyError>)?
func removeAvatar() async -> Result<Void, RoomProxyError> {
removeAvatarCallsCount += 1
if let removeAvatarClosure = removeAvatarClosure {
return await removeAvatarClosure()
} else {
return removeAvatarReturnValue
}
}
//MARK: - uploadAvatar
var uploadAvatarMediaCallsCount = 0
var uploadAvatarMediaCalled: Bool {
return uploadAvatarMediaCallsCount > 0
}
var uploadAvatarMediaReceivedMedia: MediaInfo?
var uploadAvatarMediaReceivedInvocations: [MediaInfo] = []
var uploadAvatarMediaReturnValue: Result<Void, RoomProxyError>!
var uploadAvatarMediaClosure: ((MediaInfo) async -> Result<Void, RoomProxyError>)?
func uploadAvatar(media: MediaInfo) async -> Result<Void, RoomProxyError> {
uploadAvatarMediaCallsCount += 1
uploadAvatarMediaReceivedMedia = media
uploadAvatarMediaReceivedInvocations.append(media)
if let uploadAvatarMediaClosure = uploadAvatarMediaClosure {
return await uploadAvatarMediaClosure(media)
} else {
return uploadAvatarMediaReturnValue
}
}
} }
class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol { class SessionVerificationControllerProxyMock: SessionVerificationControllerProxyProtocol {
var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> { var callbacks: PassthroughSubject<SessionVerificationControllerProxyCallback, Never> {
@@ -23,11 +23,12 @@ struct RoomMemberProxyMockConfiguration {
var avatarURL: URL? var avatarURL: URL?
var membership: MembershipState var membership: MembershipState
var isNameAmbiguous = false var isNameAmbiguous = false
var powerLevel: Int var powerLevel = 50
var normalizedPowerLevel: Int var normalizedPowerLevel = 50
var isAccountOwner = false var isAccountOwner = false
var isIgnored = false var isIgnored = false
var canInviteUsers = false var canInviteUsers = false
var canSendStateEvent: (StateEventType) -> Bool = { _ in true }
} }
extension RoomMemberProxyMock { extension RoomMemberProxyMock {
@@ -43,6 +44,7 @@ extension RoomMemberProxyMock {
isAccountOwner = configuration.isAccountOwner isAccountOwner = configuration.isAccountOwner
isIgnored = configuration.isIgnored isIgnored = configuration.isIgnored
canInviteUsers = configuration.canInviteUsers canInviteUsers = configuration.canInviteUsers
canSendStateEventTypeClosure = configuration.canSendStateEvent
} }
// Mocks // Mocks
@@ -50,45 +52,35 @@ extension RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org", RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org",
displayName: "Alice", displayName: "Alice",
avatarURL: nil, avatarURL: nil,
membership: .join, membership: .join))
powerLevel: 50,
normalizedPowerLevel: 50))
} }
static var mockInvitedAlice: RoomMemberProxyMock { static var mockInvitedAlice: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org", RoomMemberProxyMock(with: .init(userID: "@alice:matrix.org",
displayName: "Alice", displayName: "Alice",
avatarURL: nil, avatarURL: nil,
membership: .invite, membership: .invite))
powerLevel: 50,
normalizedPowerLevel: 50))
} }
static var mockBob: RoomMemberProxyMock { static var mockBob: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@bob:matrix.org", RoomMemberProxyMock(with: .init(userID: "@bob:matrix.org",
displayName: "Bob", displayName: "Bob",
avatarURL: nil, avatarURL: nil,
membership: .join, membership: .join))
powerLevel: 50,
normalizedPowerLevel: 50))
} }
static var mockCharlie: RoomMemberProxyMock { static var mockCharlie: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@charlie:matrix.org", RoomMemberProxyMock(with: .init(userID: "@charlie:matrix.org",
displayName: "Charlie", displayName: "Charlie",
avatarURL: nil, avatarURL: nil,
membership: .join, membership: .join))
powerLevel: 50,
normalizedPowerLevel: 50))
} }
static var mockDan: RoomMemberProxyMock { static var mockDan: RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@dan:matrix.org", RoomMemberProxyMock(with: .init(userID: "@dan:matrix.org",
displayName: "Dan", displayName: "Dan",
avatarURL: URL.picturesDirectory, avatarURL: URL.picturesDirectory,
membership: .join, membership: .join))
powerLevel: 50,
normalizedPowerLevel: 50))
} }
static var mockMe: RoomMemberProxyMock { static var mockMe: RoomMemberProxyMock {
@@ -96,8 +88,6 @@ extension RoomMemberProxyMock {
displayName: "Me", displayName: "Me",
avatarURL: URL.picturesDirectory, avatarURL: URL.picturesDirectory,
membership: .join, membership: .join,
powerLevel: 50,
normalizedPowerLevel: 50,
isAccountOwner: true, isAccountOwner: true,
canInviteUsers: true)) canInviteUsers: true))
} }
@@ -107,8 +97,14 @@ extension RoomMemberProxyMock {
displayName: "Ignored", displayName: "Ignored",
avatarURL: nil, avatarURL: nil,
membership: .join, membership: .join,
powerLevel: 50,
normalizedPowerLevel: 50,
isIgnored: true)) isIgnored: true))
} }
static func mockOwner(allowedStateEvents: [StateEventType]) -> RoomMemberProxyMock {
RoomMemberProxyMock(with: .init(userID: "@foo:some.org",
displayName: "User owner",
membership: .join,
isAccountOwner: true,
canSendStateEvent: { allowedStateEvents.contains($0) }))
}
} }
+3 -1
View File
@@ -19,7 +19,7 @@ import Foundation
struct RoomProxyMockConfiguration { struct RoomProxyMockConfiguration {
var id = UUID().uuidString var id = UUID().uuidString
let name: String? = nil var name: String?
let displayName: String? let displayName: String?
var topic: String? var topic: String?
var avatarURL: URL? var avatarURL: URL?
@@ -75,5 +75,7 @@ extension RoomProxyMock {
acceptInvitationClosure = { .success(()) } acceptInvitationClosure = { .success(()) }
registerTimelineListenerIfNeededClosure = { .success([]) } registerTimelineListenerIfNeededClosure = { .success([]) }
underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher() underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) }
} }
} }
@@ -94,6 +94,7 @@ struct A11yIdentifiers {
} }
struct RoomDetailsScreen { struct RoomDetailsScreen {
let addTopic = "room_details-add_topic"
let avatar = "room_details-avatar" let avatar = "room_details-avatar"
let dmAvatar = "room_details-dm_avatar" let dmAvatar = "room_details-dm_avatar"
let people = "room_details-people" let people = "room_details-people"
@@ -0,0 +1,59 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct OverridableAvatarImage: View {
private let overrideURL: URL?
private let url: URL?
private let name: String?
private let contentID: String?
private let avatarSize: AvatarSize
private let imageProvider: ImageProviderProtocol?
@ScaledMetric private var frameSize: CGFloat
init(overrideURL: URL?, url: URL?, name: String?, contentID: String?, avatarSize: AvatarSize, imageProvider: ImageProviderProtocol?) {
self.overrideURL = overrideURL
self.url = url
self.name = name
self.contentID = contentID
self.avatarSize = avatarSize
self.imageProvider = imageProvider
_frameSize = ScaledMetric(wrappedValue: avatarSize.value)
}
var body: some View {
if let overrideURL {
AsyncImage(url: overrideURL) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
} placeholder: {
ProgressView()
}
.frame(width: frameSize, height: frameSize)
.clipShape(Circle())
} else {
LoadableAvatarImage(url: url,
name: name,
contentID: contentID,
avatarSize: avatarSize,
imageProvider: imageProvider)
}
}
}
@@ -67,7 +67,7 @@ struct UserProfileCell: View {
Text(Image(systemName: "exclamationmark.circle.fill")) Text(Image(systemName: "exclamationmark.circle.fill"))
.foregroundColor(.compound.textCriticalPrimary) .foregroundColor(.compound.textCriticalPrimary)
Text(L10n.screenStartChatUnknownProfile) Text(L10n.commonInviteUnknownProfile)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
.font(.compound.bodyXS) .font(.compound.bodyXS)
@@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import Combine
import Foundation import Foundation
class MockUserIndicatorController: UserIndicatorControllerProtocol { class MockUserIndicatorController: UserIndicatorControllerProtocol {
@@ -23,5 +24,11 @@ class MockUserIndicatorController: UserIndicatorControllerProtocol {
func retractAllIndicators() { } func retractAllIndicators() { }
var alertInfo: AlertInfo<UUID>? var alertInfo: AlertInfo<UUID>? {
didSet {
alertInfoPublisher.send(alertInfo)
}
}
let alertInfoPublisher: PassthroughSubject<AlertInfo<UUID>?, Never> = .init()
} }
@@ -30,6 +30,7 @@ struct DeveloperOptionsScreenViewStateBindings {
var startChatUserSuggestionsEnabled: Bool var startChatUserSuggestionsEnabled: Bool
var invitesFlowEnabled: Bool var invitesFlowEnabled: Bool
var inviteMorePeopleFlowEnabled: Bool var inviteMorePeopleFlowEnabled: Bool
var editRoomDetailsFlowEnabled: Bool
} }
enum DeveloperOptionsScreenViewAction { enum DeveloperOptionsScreenViewAction {
@@ -38,5 +39,6 @@ enum DeveloperOptionsScreenViewAction {
case changedStartChatUserSuggestionsEnabled case changedStartChatUserSuggestionsEnabled
case changedInvitesFlowEnabled case changedInvitesFlowEnabled
case changedInviteMorePeopleFlowEnabled case changedInviteMorePeopleFlowEnabled
case changedEditRoomDetailsFlowEnabled
case clearCache case clearCache
} }
@@ -29,7 +29,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
startChatFlowEnabled: appSettings.startChatFlowEnabled, startChatFlowEnabled: appSettings.startChatFlowEnabled,
startChatUserSuggestionsEnabled: appSettings.startChatUserSuggestionsEnabled, startChatUserSuggestionsEnabled: appSettings.startChatUserSuggestionsEnabled,
invitesFlowEnabled: appSettings.invitesFlowEnabled, invitesFlowEnabled: appSettings.invitesFlowEnabled,
inviteMorePeopleFlowEnabled: appSettings.inviteMorePeopleFlowEnabled) inviteMorePeopleFlowEnabled: appSettings.inviteMorePeopleFlowEnabled,
editRoomDetailsFlowEnabled: appSettings.editRoomDetailsFlowEnabled)
let state = DeveloperOptionsScreenViewState(bindings: bindings) let state = DeveloperOptionsScreenViewState(bindings: bindings)
super.init(initialViewState: state) super.init(initialViewState: state)
@@ -51,6 +52,8 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
appSettings.invitesFlowEnabled = state.bindings.invitesFlowEnabled appSettings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
case .changedInviteMorePeopleFlowEnabled: case .changedInviteMorePeopleFlowEnabled:
appSettings.inviteMorePeopleFlowEnabled = state.bindings.inviteMorePeopleFlowEnabled appSettings.inviteMorePeopleFlowEnabled = state.bindings.inviteMorePeopleFlowEnabled
case .changedEditRoomDetailsFlowEnabled:
appSettings.editRoomDetailsFlowEnabled = state.bindings.editRoomDetailsFlowEnabled
case .clearCache: case .clearCache:
callback?(.clearCache) callback?(.clearCache)
} }
@@ -57,6 +57,13 @@ struct DeveloperOptionsScreen: View {
.onChange(of: context.inviteMorePeopleFlowEnabled) { _ in .onChange(of: context.inviteMorePeopleFlowEnabled) { _ in
context.send(viewAction: .changedInviteMorePeopleFlowEnabled) context.send(viewAction: .changedInviteMorePeopleFlowEnabled)
} }
Toggle(isOn: $context.editRoomDetailsFlowEnabled) {
Text("Show edit room details flow")
}
.onChange(of: context.editRoomDetailsFlowEnabled) { _ in
context.send(viewAction: .changedEditRoomDetailsFlowEnabled)
}
} }
Section { Section {
@@ -0,0 +1,90 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
struct RoomDetailsEditScreenCoordinatorParameters {
let accountOwner: RoomMemberProxyProtocol
let mediaProvider: MediaProviderProtocol
let navigationStackCoordinator: NavigationStackCoordinator
let roomProxy: RoomProxyProtocol
let userIndicatorController: UserIndicatorControllerProtocol
}
enum RoomDetailsEditScreenCoordinatorAction {
case dismiss
}
final class RoomDetailsEditScreenCoordinator: CoordinatorProtocol {
private let parameters: RoomDetailsEditScreenCoordinatorParameters
private var viewModel: RoomDetailsEditScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenCoordinatorAction, Never> = .init()
private var cancellables: Set<AnyCancellable> = .init()
var actions: AnyPublisher<RoomDetailsEditScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: RoomDetailsEditScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = RoomDetailsEditScreenViewModel(accountOwner: parameters.accountOwner,
mediaProvider: parameters.mediaProvider,
roomProxy: parameters.roomProxy,
userIndicatorController: parameters.userIndicatorController)
}
func start() {
viewModel.actions
.sink { [weak self] action in
switch action {
case .cancel, .saveFinished:
self?.actionsSubject.send(.dismiss)
case .displayCameraPicker:
self?.displayMediaPickerWithSource(.camera)
case .displayMediaPicker:
self?.displayMediaPickerWithSource(.photoLibrary)
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(RoomDetailsEditScreen(context: viewModel.context))
}
// MARK: Private
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
let stackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: stackCoordinator)
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, source: source) { [weak self] action in
guard let self else { return }
switch action {
case .cancel:
parameters.navigationStackCoordinator.setSheetCoordinator(nil)
case .selectMediaAtURL(let url):
parameters.navigationStackCoordinator.setSheetCoordinator(nil)
viewModel.didSelectMediaUrl(url: url)
}
}
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
}
}
@@ -0,0 +1,73 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum RoomDetailsEditScreenViewModelAction {
case cancel
case saveFinished
case displayCameraPicker
case displayMediaPicker
}
struct RoomDetailsEditScreenViewStateBindings {
var name: String
var topic: String
var showMediaSheet = false
}
struct RoomDetailsEditScreenViewState: BindableState {
let roomID: String
let initialAvatarURL: URL?
let initialName: String
let initialTopic: String
let canEditAvatar: Bool
let canEditName: Bool
let canEditTopic: Bool
var avatarURL: URL?
var localMedia: MediaInfo?
var bindings: RoomDetailsEditScreenViewStateBindings
var nameDidChange: Bool {
bindings.name != initialName
}
var topicDidChange: Bool {
bindings.topic != initialTopic
}
var avatarDidChange: Bool {
localMedia != nil || avatarURL != initialAvatarURL
}
var canSave: Bool {
!bindings.name.isEmpty && (avatarDidChange || nameDidChange || topicDidChange)
}
var showDeleteImageAction: Bool {
localMedia != nil || avatarURL != nil
}
}
enum RoomDetailsEditScreenViewAction {
case cancel
case save
case presentMediaSource
case displayCameraPicker
case displayMediaPicker
case removeImage
}
@@ -0,0 +1,141 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import SwiftUI
typealias RoomDetailsEditScreenViewModelType = StateStoreViewModel<RoomDetailsEditScreenViewState, RoomDetailsEditScreenViewAction>
class RoomDetailsEditScreenViewModel: RoomDetailsEditScreenViewModelType, RoomDetailsEditScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<RoomDetailsEditScreenViewModelAction, Never> = .init()
private let roomProxy: RoomProxyProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let mediaPreprocessor: MediaUploadingPreprocessor = .init()
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(accountOwner: RoomMemberProxyProtocol,
mediaProvider: MediaProviderProtocol,
roomProxy: RoomProxyProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.roomProxy = roomProxy
self.userIndicatorController = userIndicatorController
let roomAvatar = roomProxy.avatarURL
let roomName = roomProxy.name
let roomTopic = roomProxy.topic
super.init(initialViewState: RoomDetailsEditScreenViewState(roomID: roomProxy.id,
initialAvatarURL: roomAvatar,
initialName: roomName ?? "",
initialTopic: roomTopic ?? "",
canEditAvatar: accountOwner.canSendStateEvent(type: .roomAvatar),
canEditName: accountOwner.canSendStateEvent(type: .roomName),
canEditTopic: accountOwner.canSendStateEvent(type: .roomTopic),
avatarURL: roomAvatar,
bindings: .init(name: roomName ?? "", topic: roomTopic ?? "")), imageProvider: mediaProvider)
}
// MARK: - Public
override func process(viewAction: RoomDetailsEditScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.cancel)
case .save:
saveRoomDetails()
case .presentMediaSource:
state.bindings.showMediaSheet = true
case .displayCameraPicker:
actionsSubject.send(.displayCameraPicker)
case .displayMediaPicker:
actionsSubject.send(.displayMediaPicker)
case .removeImage:
if state.localMedia != nil {
state.localMedia = nil
} else {
state.avatarURL = nil
}
}
}
func didSelectMediaUrl(url: URL) {
Task {
let userIndicatorID = UUID().uuidString
defer {
userIndicatorController.retractIndicatorWithId(userIndicatorID)
}
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.commonLoading, persistent: true))
let mediaResult = await mediaPreprocessor.processMedia(at: url)
switch mediaResult {
case .success(.image):
state.localMedia = try? mediaResult.get()
case .failure, .success:
userIndicatorController.alertInfo = .init(id: .init(), title: L10n.commonError, message: L10n.errorUnknown)
}
}
}
// MARK: - Private
private func saveRoomDetails() {
Task {
let userIndicatorID = UUID().uuidString
defer {
userIndicatorController.retractIndicatorWithId(userIndicatorID)
}
userIndicatorController.submitIndicator(UserIndicator(id: userIndicatorID, type: .modal, title: L10n.screenRoomDetailsUpdatingRoom, persistent: true))
do {
try await withThrowingTaskGroup(of: Void.self) { group in
if state.avatarDidChange {
group.addTask {
if let localMedia = await self.state.localMedia {
try await self.roomProxy.uploadAvatar(media: localMedia).get()
} else if await self.state.avatarURL == nil {
try await self.roomProxy.removeAvatar().get()
}
}
}
if state.nameDidChange {
group.addTask {
try await self.roomProxy.setName(self.state.bindings.name).get()
}
}
if state.topicDidChange {
group.addTask {
try await self.roomProxy.setTopic(self.state.bindings.topic).get()
}
}
try await group.waitForAll()
}
actionsSubject.send(.saveFinished)
} catch {
userIndicatorController.alertInfo = .init(id: .init(),
title: L10n.screenRoomDetailsEditionErrorTitle,
message: L10n.screenRoomDetailsEditionError)
}
}
}
}
@@ -0,0 +1,26 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Combine
import Foundation
@MainActor
protocol RoomDetailsEditScreenViewModelProtocol {
var actions: AnyPublisher<RoomDetailsEditScreenViewModelAction, Never> { get }
var context: RoomDetailsEditScreenViewModelType.Context { get }
func didSelectMediaUrl(url: URL)
}
@@ -0,0 +1,176 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
struct RoomDetailsEditScreen: View {
@ObservedObject var context: RoomDetailsEditScreenViewModel.Context
@FocusState private var focus: Focus?
private enum Focus {
case name
case topic
}
var body: some View {
mainContent
.scrollContentBackground(.hidden)
.background(Color.element.formBackground.ignoresSafeArea())
.scrollDismissesKeyboard(.immediately)
.navigationTitle(L10n.screenRoomDetailsEditRoomTitle)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(L10n.actionSave) {
context.send(viewAction: .save)
focus = nil
}
.disabled(!context.viewState.canSave)
}
}
}
// MARK: - Private
private var mainContent: some View {
Form {
avatar
nameSection
topicSection
}
}
private var avatar: some View {
Button {
context.send(viewAction: .presentMediaSource)
} label: {
OverridableAvatarImage(overrideURL: context.viewState.localMedia?.thumbnailURL,
url: context.viewState.avatarURL,
name: context.viewState.initialName,
contentID: context.viewState.roomID,
avatarSize: .user(on: .memberDetails),
imageProvider: context.imageProvider)
.overlay(alignment: .bottomTrailing) {
if context.viewState.canEditAvatar {
avatarOverlayIcon
}
}
.confirmationDialog("", isPresented: $context.showMediaSheet) {
mediaActionSheet
}
}
.buttonStyle(.plain)
.disabled(!context.viewState.canEditAvatar)
.frame(maxWidth: .infinity, alignment: .center)
.listRowBackground(Color.clear)
}
private var nameSection: some View {
Section {
let canEditName = context.viewState.canEditName
TextField(L10n.commonRoomName,
text: $context.name,
prompt: canEditName ? Text(L10n.screenCreateRoomRoomNamePlaceholder) : nil,
axis: .horizontal)
.focused($focus, equals: .name)
.font(.compound.bodyLG)
.foregroundColor(.element.primaryContent)
.disabled(!canEditName)
.listRowBackground(canEditName ? nil : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 8))
} header: {
Text(L10n.commonRoomName)
.formSectionHeader()
}
.formSectionStyle()
}
private var topicSection: some View {
Section {
let canEditTopic = context.viewState.canEditTopic
TextField(L10n.commonTopic,
text: $context.topic,
prompt: canEditTopic ? Text(L10n.screenCreateRoomTopicPlaceholder) : nil,
axis: .vertical)
.focused($focus, equals: .topic)
.font(.compound.bodyLG)
.foregroundColor(.element.primaryContent)
.disabled(!canEditTopic)
.listRowBackground(canEditTopic ? nil : Color.clear)
.lineLimit(3, reservesSpace: true)
} header: {
Text(L10n.commonTopic)
.formSectionHeader()
}
.formSectionStyle()
}
private var avatarOverlayIcon: some View {
Image(systemName: "camera")
.padding(2)
.imageScale(.small)
.foregroundColor(.white)
.background {
Circle()
.foregroundColor(.black)
.aspectRatio(1, contentMode: .fill)
}
}
@ViewBuilder
private var mediaActionSheet: some View {
Button {
context.send(viewAction: .displayCameraPicker)
} label: {
Text(L10n.actionTakePhoto)
}
Button {
context.send(viewAction: .displayMediaPicker)
} label: {
Text(L10n.actionChoosePhoto)
}
if context.viewState.showDeleteImageAction {
Button(role: .destructive) {
context.send(viewAction: .removeImage)
} label: {
Text(L10n.actionRemove)
}
}
}
}
// MARK: - Previews
struct RoomDetailsEditScreen_Previews: PreviewProvider {
static let viewModel = RoomDetailsEditScreenViewModel(accountOwner: RoomMemberProxyMock.mockAlice,
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: .init(name: "Room", displayName: "Room")),
userIndicatorController: MockUserIndicatorController())
static var previews: some View {
NavigationStack {
RoomDetailsEditScreen(context: viewModel.context)
}
}
}
@@ -62,6 +62,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
self.callback?(.cancel) self.callback?(.cancel)
case .leftRoom: case .leftRoom:
self.callback?(.leftRoom) self.callback?(.leftRoom)
case .requestEditDetailsPresentation(let accountOwner):
self.presentRoomDetailsEditScreen(accountOwner: accountOwner)
} }
} }
} }
@@ -116,6 +118,30 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController) parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
} }
private func presentRoomDetailsEditScreen(accountOwner: RoomMemberProxyProtocol) {
let navigationStackCoordinator = NavigationStackCoordinator()
let userIndicatorController = UserIndicatorController(rootCoordinator: navigationStackCoordinator)
let roomDetailsEditParameters = RoomDetailsEditScreenCoordinatorParameters(accountOwner: accountOwner,
mediaProvider: parameters.mediaProvider,
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: parameters.roomProxy,
userIndicatorController: userIndicatorController)
let roomDetailsEditCoordinator = RoomDetailsEditScreenCoordinator(parameters: roomDetailsEditParameters)
roomDetailsEditCoordinator.actions.sink { [weak self] action in
switch action {
case .dismiss:
self?.parameters.navigationStackCoordinator.setSheetCoordinator(nil)
}
}
.store(in: &cancellables)
navigationStackCoordinator.setRootCoordinator(roomDetailsEditCoordinator)
parameters.navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
}
private func toggleUser(_ user: UserProfile) { private func toggleUser(_ user: UserProfile) {
var selectedUsers = selectedUsers.value var selectedUsers = selectedUsers.value
if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) { if let index = selectedUsers.firstIndex(where: { $0.userID == user.userID }) {
@@ -26,6 +26,7 @@ enum RoomDetailsScreenViewModelAction {
case requestInvitePeoplePresentation([RoomMemberProxyProtocol]) case requestInvitePeoplePresentation([RoomMemberProxyProtocol])
case leftRoom case leftRoom
case cancel case cancel
case requestEditDetailsPresentation(RoomMemberProxyProtocol)
} }
// MARK: View // MARK: View
@@ -43,11 +44,22 @@ struct RoomDetailsScreenViewState: BindableState {
var joinedMembersCount = 0 var joinedMembersCount = 0
var isProcessingIgnoreRequest = false var isProcessingIgnoreRequest = false
var canInviteUsers = false var canInviteUsers = false
var canEditRoomName = false
var canEditRoomTopic = false
var canEditRoomAvatar = false
var isLoadingMembers: Bool { var isLoadingMembers: Bool {
members.isEmpty members.isEmpty
} }
var canEdit: Bool {
!isDirect && ServiceLocator.shared.settings.editRoomDetailsFlowEnabled && (canEditRoomName || canEditRoomTopic || canEditRoomAvatar)
}
var hasTopicSection: Bool {
topic != nil || (canEdit && canEditRoomTopic)
}
var bindings: RoomDetailsScreenViewStateBindings var bindings: RoomDetailsScreenViewStateBindings
var dmRecipient: RoomMemberDetails? var dmRecipient: RoomMemberDetails?
@@ -130,6 +142,8 @@ enum RoomDetailsScreenViewAction {
case processTapLeave case processTapLeave
case processTapIgnore case processTapIgnore
case processTapUnignore case processTapUnignore
case processTapEdit
case processTapAddTopic
case confirmLeave case confirmLeave
case ignoreConfirmed case ignoreConfirmed
case unignoreConfirmed case unignoreConfirmed
@@ -23,6 +23,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private let roomProxy: RoomProxyProtocol private let roomProxy: RoomProxyProtocol
private var members: [RoomMemberProxyProtocol] = [] private var members: [RoomMemberProxyProtocol] = []
private var dmRecipient: RoomMemberProxyProtocol? private var dmRecipient: RoomMemberProxyProtocol?
private var accountOwner: RoomMemberProxyProtocol?
@CancellableTask @CancellableTask
private var buildMembersDetailsTask: Task<Void, Never>? private var buildMembersDetailsTask: Task<Void, Never>?
@@ -52,6 +53,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// MARK: - Public // MARK: - Public
// swiftlint:disable:next cyclomatic_complexity
override func process(viewAction: RoomDetailsScreenViewAction) { override func process(viewAction: RoomDetailsScreenViewAction) {
switch viewAction { switch viewAction {
case .processTapPeople: case .processTapPeople:
@@ -71,6 +73,12 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
state.bindings.ignoreUserRoomAlertItem = .init(action: .ignore) state.bindings.ignoreUserRoomAlertItem = .init(action: .ignore)
case .processTapUnignore: case .processTapUnignore:
state.bindings.ignoreUserRoomAlertItem = .init(action: .unignore) state.bindings.ignoreUserRoomAlertItem = .init(action: .unignore)
case .processTapEdit, .processTapAddTopic:
guard let accountOwner else {
MXLog.error("Missing account owner when presenting the room's edit details screen")
return
}
callback?(.requestEditDetailsPresentation(accountOwner))
case .ignoreConfirmed: case .ignoreConfirmed:
Task { await ignore() } Task { await ignore() }
case .unignoreConfirmed: case .unignoreConfirmed:
@@ -105,13 +113,17 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount self.state.joinedMembersCount = roomMembersDetails.joinedMembersCount
self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:)) self.state.dmRecipient = self.dmRecipient.map(RoomMemberDetails.init(withProxy:))
self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false self.state.canInviteUsers = roomMembersDetails.accountOwner?.canInviteUsers ?? false
self.state.canEditRoomName = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomName) ?? false
self.state.canEditRoomTopic = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomTopic) ?? false
self.state.canEditRoomAvatar = roomMembersDetails.accountOwner?.canSendStateEvent(type: .roomAvatar) ?? false
self.members = members self.members = members
self.accountOwner = roomMembersDetails.accountOwner
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
roomProxy.updatesPublisher roomProxy.updatesPublisher
.throttle(for: .seconds(1), scheduler: DispatchQueue.main, latest: true) .throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in .sink { [weak self] _ in
guard let self else { return } guard let self else { return }
self.state.title = self.roomProxy.roomTitle self.state.title = self.roomProxy.roomTitle
@@ -50,6 +50,15 @@ struct RoomDetailsScreen: View {
.alert(item: $context.ignoreUserRoomAlertItem, .alert(item: $context.ignoreUserRoomAlertItem,
actions: blockUserAlertActions, actions: blockUserAlertActions,
message: blockUserAlertMessage) message: blockUserAlertMessage)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if context.viewState.canEdit {
Button(L10n.actionEdit) {
context.send(viewAction: .processTapEdit)
}
}
}
}
} }
// MARK: - Private // MARK: - Private
@@ -98,11 +107,23 @@ struct RoomDetailsScreen: View {
@ViewBuilder @ViewBuilder
private var topicSection: some View { private var topicSection: some View {
if let topic = context.viewState.topic { if context.viewState.hasTopicSection {
Section { Section {
if let topic = context.viewState.topic, !topic.isEmpty {
Text(topic) Text(topic)
.foregroundColor(.element.secondaryContent) .foregroundColor(.element.secondaryContent)
.font(.compound.bodySM) .font(.compound.bodySM)
.lineLimit(3)
} else {
Button {
context.send(viewAction: .processTapAddTopic)
} label: {
Text(L10n.screenRoomDetailsAddTopicTitle)
.foregroundColor(.element.primaryContent)
.font(.compound.bodyLG)
}
.accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.addTopic)
}
} header: { } header: {
Text(L10n.commonTopic) Text(L10n.commonTopic)
.formSectionHeader() .formSectionHeader()
@@ -142,7 +163,6 @@ struct RoomDetailsScreen: View {
.listRowSeparatorTint(.element.quinaryContent) .listRowSeparatorTint(.element.quinaryContent)
.buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink)) .buttonStyle(FormButtonStyle(accessory: context.viewState.isLoadingMembers ? nil : .navigationLink))
.foregroundColor(.element.primaryContent) .foregroundColor(.element.primaryContent)
.disabled(context.viewState.isLoadingMembers) .disabled(context.viewState.isLoadingMembers)
} }
@@ -41,6 +41,38 @@ enum MediaInfo {
case video(videoURL: URL, thumbnailURL: URL, videoInfo: VideoInfo) case video(videoURL: URL, thumbnailURL: URL, videoInfo: VideoInfo)
case audio(audioURL: URL, audioInfo: AudioInfo) case audio(audioURL: URL, audioInfo: AudioInfo)
case file(fileURL: URL, fileInfo: FileInfo) case file(fileURL: URL, fileInfo: FileInfo)
var mimeType: String? {
switch self {
case .image(_, _, let imageInfo):
return imageInfo.mimetype
case .video(_, _, let videoInfo):
return videoInfo.mimetype
case .audio(_, let audioInfo):
return audioInfo.mimetype
case .file(_, let fileInfo):
return fileInfo.mimetype
}
}
var url: URL {
switch self {
case .image(let url, _, _),
.video(let url, _, _),
.audio(let url, _),
.file(let url, _):
return url
}
}
var thumbnailURL: URL? {
switch self {
case .image(_, let url, _), .video(_, let url, _):
return url
case .audio, .file:
return nil
}
}
} }
private struct ImageProcessingInfo { private struct ImageProcessingInfo {
@@ -71,6 +71,10 @@ final class RoomMemberProxy: RoomMemberProxyProtocol {
member.canInvite() member.canInvite()
} }
func canSendStateEvent(type: StateEventType) -> Bool {
member.canSendState(stateEvent: type)
}
func ignoreUser() async -> Result<Void, RoomMemberProxyError> { func ignoreUser() async -> Result<Void, RoomMemberProxyError> {
sendAccountDataEventBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true) sendAccountDataEventBackgroundTask = await backgroundTaskService.startBackgroundTask(withName: backgroundAccountDataTaskName, isReusable: true)
defer { defer {
@@ -37,6 +37,7 @@ protocol RoomMemberProxyProtocol {
func ignoreUser() async -> Result<Void, RoomMemberProxyError> func ignoreUser() async -> Result<Void, RoomMemberProxyError>
func unignoreUser() async -> Result<Void, RoomMemberProxyError> func unignoreUser() async -> Result<Void, RoomMemberProxyError>
func canSendStateEvent(type: StateEventType) -> Bool
} }
extension RoomMemberProxyProtocol { extension RoomMemberProxyProtocol {
+51 -6
View File
@@ -131,6 +131,18 @@ class RoomProxy: RoomProxyProtocol {
return Asset.Images.encryptionTrusted.image return Asset.Images.encryptionTrusted.image
} }
var invitedMembersCount: UInt {
UInt(room.invitedMembersCount())
}
var joinedMembersCount: UInt {
UInt(room.joinedMembersCount())
}
var activeMembersCount: UInt {
UInt(room.activeMembersCount())
}
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> { func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError> {
do { do {
guard let urlString = try await Task.dispatch(on: lowPriorityDispatchQueue, { guard let urlString = try await Task.dispatch(on: lowPriorityDispatchQueue, {
@@ -486,16 +498,49 @@ class RoomProxy: RoomProxyProtocol {
} }
} }
var invitedMembersCount: UInt { func setName(_ name: String?) async -> Result<Void, RoomProxyError> {
UInt(room.invitedMembersCount()) await Task.dispatch(on: .global()) {
do {
return try .success(self.room.setName(name: name))
} catch {
return .failure(.failedSettingRoomName)
}
}
} }
var joinedMembersCount: UInt { func setTopic(_ topic: String) async -> Result<Void, RoomProxyError> {
UInt(room.joinedMembersCount()) await Task.dispatch(on: .global()) {
do {
return try .success(self.room.setTopic(topic: topic))
} catch {
return .failure(.failedSettingRoomTopic)
}
}
} }
var activeMembersCount: UInt { func removeAvatar() async -> Result<Void, RoomProxyError> {
UInt(room.activeMembersCount()) await Task.dispatch(on: .global()) {
do {
return try .success(self.room.removeAvatar())
} catch {
return .failure(.failedRemovingAvatar)
}
}
}
func uploadAvatar(media: MediaInfo) async -> Result<Void, RoomProxyError> {
await Task.dispatch(on: .global()) {
guard case let .image(imageURL, _, _) = media, let mimeType = media.mimeType else {
return .failure(.failedUploadingAvatar)
}
do {
let data = try Data(contentsOf: imageURL)
return try .success(self.room.uploadAvatar(mimeType: mimeType, data: [UInt8](data)))
} catch {
return .failure(.failedUploadingAvatar)
}
}
} }
// MARK: - Private // MARK: - Private
@@ -37,6 +37,10 @@ enum RoomProxyError: Error {
case failedAcceptingInvite case failedAcceptingInvite
case failedRejectingInvite case failedRejectingInvite
case failedInvitingUser case failedInvitingUser
case failedSettingRoomName
case failedSettingRoomTopic
case failedRemovingAvatar
case failedUploadingAvatar
} }
@MainActor @MainActor
@@ -61,6 +65,12 @@ protocol RoomProxyProtocol {
var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get } var membersPublisher: AnyPublisher<[RoomMemberProxyProtocol], Never> { get }
var invitedMembersCount: UInt { get }
var joinedMembersCount: UInt { get }
var activeMembersCount: UInt { get }
/// Publishes the room's updates. /// Publishes the room's updates.
/// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()` /// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()`
/// The thread on which this publisher sends the output isn't defined. /// The thread on which this publisher sends the output isn't defined.
@@ -114,11 +124,13 @@ protocol RoomProxyProtocol {
func invite(userID: String) async -> Result<Void, RoomProxyError> func invite(userID: String) async -> Result<Void, RoomProxyError>
var invitedMembersCount: UInt { get } func setName(_ name: String?) async -> Result<Void, RoomProxyError>
var joinedMembersCount: UInt { get } func setTopic(_ topic: String) async -> Result<Void, RoomProxyError>
var activeMembersCount: UInt { get } func removeAvatar() async -> Result<Void, RoomProxyError>
func uploadAvatar(media: MediaInfo) async -> Result<Void, RoomProxyError>
} }
extension RoomProxyProtocol { extension RoomProxyProtocol {
@@ -15,6 +15,7 @@
// //
import Combine import Combine
import MatrixRustSDK
import SwiftUI import SwiftUI
import UIKit import UIKit
@@ -321,6 +322,24 @@ class MockScreen: Identifiable {
userDiscoveryService: UserDiscoveryServiceMock())) userDiscoveryService: UserDiscoveryServiceMock()))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .roomDetailsScreenWithEmptyTopic:
ServiceLocator.shared.settings.editRoomDetailsFlowEnabled = true
let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockCharlie]
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
displayName: "Room",
topic: nil,
avatarURL: URL.picturesDirectory,
isDirect: false,
isEncrypted: true,
canonicalAlias: "#mock:room.org",
members: members))
let coordinator = RoomDetailsScreenCoordinator(parameters: .init(navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userDiscoveryService: UserDiscoveryServiceMock()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenWithInvite: case .roomDetailsScreenWithInvite:
ServiceLocator.shared.settings.inviteMorePeopleFlowEnabled = true ServiceLocator.shared.settings.inviteMorePeopleFlowEnabled = true
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
@@ -335,6 +354,21 @@ class MockScreen: Identifiable {
userDiscoveryService: UserDiscoveryServiceMock())) userDiscoveryService: UserDiscoveryServiceMock()))
navigationStackCoordinator.setRootCoordinator(coordinator) navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator return navigationStackCoordinator
case .roomEditDetails, .roomEditDetailsReadOnly:
let allowedStateEvents: [StateEventType] = id == .roomEditDetails ? [.roomAvatar, .roomName, .roomTopic] : []
let navigationStackCoordinator = NavigationStackCoordinator()
let roomProxy = RoomProxyMock(with: .init(id: "MockRoomIdentifier",
name: "Room",
displayName: "Room",
topic: "What a cool topic!",
avatarURL: .picturesDirectory))
let coordinator = RoomDetailsEditScreenCoordinator(parameters: .init(accountOwner: RoomMemberProxyMock.mockOwner(allowedStateEvents: allowedStateEvents),
mediaProvider: MockMediaProvider(),
navigationStackCoordinator: navigationStackCoordinator,
roomProxy: roomProxy,
userIndicatorController: MockUserIndicatorController()))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomMembersListScreen: case .roomMembersListScreen:
let navigationStackCoordinator = NavigationStackCoordinator() let navigationStackCoordinator = NavigationStackCoordinator()
let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie] let members: [RoomMemberProxyMock] = [.mockAlice, .mockBob, .mockCharlie]
@@ -43,8 +43,11 @@ enum UITestsScreenIdentifier: String {
case userSessionScreen case userSessionScreen
case roomDetailsScreen case roomDetailsScreen
case roomDetailsScreenWithRoomAvatar case roomDetailsScreenWithRoomAvatar
case roomDetailsScreenWithEmptyTopic
case roomDetailsScreenWithInvite case roomDetailsScreenWithInvite
case roomDetailsScreenDmDetails case roomDetailsScreenDmDetails
case roomEditDetails
case roomEditDetailsReadOnly
case roomMembersListScreen case roomMembersListScreen
case roomMembersListScreenPendingInvites case roomMembersListScreenPendingInvites
case roomMemberDetailsAccountOwner case roomMemberDetailsAccountOwner
@@ -0,0 +1,31 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import ElementX
import XCTest
@MainActor
class RoomDetailsEditScreenUITests: XCTestCase {
func testEditableRoom() async throws {
let app = Application.launch(.roomEditDetails)
try await app.assertScreenshot(.roomEditDetails)
}
func testReadOnlyRoom() async throws {
let app = Application.launch(.roomEditDetailsReadOnly)
try await app.assertScreenshot(.roomEditDetailsReadOnly)
}
}
@@ -35,6 +35,13 @@ class RoomDetailsScreenUITests: XCTestCase {
try await app.assertScreenshot(.roomDetailsScreenWithRoomAvatar) try await app.assertScreenshot(.roomDetailsScreenWithRoomAvatar)
} }
func testInitialStateComponentsWithEmptyTopic() async throws {
let app = Application.launch(.roomDetailsScreenWithEmptyTopic)
XCTAssert(app.buttons[A11yIdentifiers.roomDetailsScreen.addTopic].waitForExistence(timeout: 1))
try await app.assertScreenshot(.roomDetailsScreenWithEmptyTopic)
}
func testInitialStateComponentsWithInvite() async throws { func testInitialStateComponentsWithInvite() async throws {
let app = Application.launch(.roomDetailsScreenWithInvite) let app = Application.launch(.roomDetailsScreenWithInvite)
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:412217a207ef9a3615aec0cd8c6fa6c9eb5f78aa25389b227d2bed99c1245291
size 109946
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f1c4b2bd06eca5a87809115c557c2bd56aa932d0d3beeadabea1151ca908a49
size 78981
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae566dbc1b03a1cfdb1b3b1412c79c2b0801b3f60f89180d60be7f8819782449
size 75125
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c9aee2d012e7519e63ff561684c8f12162c9fadceff533cb974e6fbdb42cb46a
size 137771
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0410d0f1a5b47c01c7fa4349c8aa10ee9a41e7fc550621ca6f10e8b1dc9e099a
size 87208
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4916268dcb63398ae44654dc5f98386102d0c9b3516fedfd75a6868dc7d77a1b
size 79861
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0f3f0e62b3f40ee32e2c0547d3bffefd66064caa2377b78e0dc28e92cf329229
size 124694
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:95180272e203386363518550eaca4ab66a5362a3514e8046b72e2d5010380fab
size 81651
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dff46af4fb867cfe29c989b5c0e4c3ec7db25f2af19ca60e4ea075f65ac719ce
size 78457
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d5b8f42166bfee1c2f1a82db462465e03ac95ea35703d4328c7b5dfd4e071ad9
size 170859
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f2e6eb2365ccd1c1a0ace52872a8e84f191736dd7b74c7898256c420e30fe73
size 90556
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:232a667d2fd91bd1746509b186db7eb9676a4732110a84e3c9543808abf1d2f5
size 82817
@@ -65,9 +65,8 @@ class InviteUsersScreenViewModelTests: XCTestCase {
_ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false }) _ = await viewModel.context.$viewState.values.first(where: { $0.membershipState.isEmpty == false })
context.send(viewAction: .toggleUser(.mockAlice)) context.send(viewAction: .toggleUser(.mockAlice))
Task { Task.detached(priority: .low) {
await Task.yield() await self.context.send(viewAction: .proceed)
context.send(viewAction: .proceed)
} }
let action = await viewModel.actions.values.first() let action = await viewModel.actions.values.first()
@@ -0,0 +1,135 @@
//
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import MatrixRustSDK
import XCTest
@testable import ElementX
@MainActor
class RoomDetailsEditScreenViewModelTests: XCTestCase {
var viewModel: RoomDetailsEditScreenViewModel!
var userIndicatorController: MockUserIndicatorController!
var context: RoomDetailsEditScreenViewModelType.Context {
viewModel.context
}
func testCannotSaveOnLanding() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
XCTAssertFalse(context.viewState.canSave)
}
func testCanEdit() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
XCTAssertTrue(context.viewState.canEditAvatar)
XCTAssertTrue(context.viewState.canEditName)
XCTAssertTrue(context.viewState.canEditTopic)
}
func testCannotEdit() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: []),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
XCTAssertFalse(context.viewState.canEditAvatar)
XCTAssertFalse(context.viewState.canEditName)
XCTAssertFalse(context.viewState.canEditTopic)
}
func testNameDidChange() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = "name"
XCTAssertTrue(context.viewState.nameDidChange)
XCTAssertTrue(context.viewState.canSave)
}
func testTopicDidChange() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.topic = "topic"
XCTAssertTrue(context.viewState.topicDidChange)
XCTAssertTrue(context.viewState.canSave)
}
func testAvatarDidChange() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room", avatarURL: .picturesDirectory))
context.send(viewAction: .removeImage)
XCTAssertTrue(context.viewState.avatarDidChange)
XCTAssertTrue(context.viewState.canSave)
}
func testEmptyNameCannotBeSaved() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = ""
XCTAssertFalse(context.viewState.canSave)
}
func testSaveShowsSheet() {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = "name"
XCTAssertFalse(context.showMediaSheet)
context.send(viewAction: .presentMediaSource)
XCTAssertTrue(context.showMediaSheet)
}
func testSaveTriggersViewModelAction() async {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
context.name = "name"
context.send(viewAction: .save)
let action = await viewModel.actions.values.first()
XCTAssertEqual(action, .saveFinished)
}
func testErrorShownOnFailedFetchOfMedia() async throws {
setupViewModel(accountOwner: .mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]),
roomProxyConfiguration: .init(name: "Some room", displayName: "Some room"))
viewModel.didSelectMediaUrl(url: .picturesDirectory)
let alertInfo = await userIndicatorController.alertInfoPublisher
.compactMap { $0 }
.values
.first()
XCTAssertNotNil(alertInfo)
}
// MARK: - Private
private func setupViewModel(accountOwner: RoomMemberProxyMock, roomProxyConfiguration: RoomProxyMockConfiguration) {
userIndicatorController = MockUserIndicatorController()
viewModel = .init(accountOwner: accountOwner,
mediaProvider: MockMediaProvider(),
roomProxy: RoomProxyMock(with: roomProxyConfiguration),
userIndicatorController: userIndicatorController)
}
}
private extension ImageInfo {
static let mock: ImageInfo = .init(height: nil,
width: nil,
mimetype: nil,
size: nil,
thumbnailInfo: nil,
thumbnailSource: nil,
blurhash: nil)
}
@@ -14,6 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import MatrixRustSDK
import XCTest import XCTest
@testable import ElementX @testable import ElementX
@@ -27,6 +28,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
override func setUp() { override func setUp() {
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test")) roomProxyMock = RoomProxyMock(with: .init(displayName: "Test"))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider()) viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
AppSettings.reset()
ServiceLocator.shared.settings.editRoomDetailsFlowEnabled = true
} }
func testLeaveRoomTappedWhenPublic() async { func testLeaveRoomTappedWhenPublic() async {
@@ -219,4 +223,66 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
await context.nextViewState() await context.nextViewState()
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1) XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
} }
func testCanEditAvatar() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertTrue(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName)
XCTAssertFalse(context.viewState.canEditRoomTopic)
XCTAssertTrue(context.viewState.canEdit)
}
func testCanEditName() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomName]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertTrue(context.viewState.canEditRoomName)
XCTAssertFalse(context.viewState.canEditRoomTopic)
XCTAssertTrue(context.viewState.canEdit)
}
func testCanEditTopic() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomTopic]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName)
XCTAssertTrue(context.viewState.canEditRoomTopic)
XCTAssertTrue(context.viewState.canEdit)
}
func testCannotEditRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: []), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: false, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEditRoomAvatar)
XCTAssertFalse(context.viewState.canEditRoomName)
XCTAssertFalse(context.viewState.canEditRoomTopic)
XCTAssertFalse(context.viewState.canEdit)
}
func testCannotEditDirectRoom() async {
let mockedMembers: [RoomMemberProxyMock] = [.mockOwner(allowedStateEvents: [.roomAvatar, .roomName, .roomTopic]), .mockBob, .mockAlice]
roomProxyMock = RoomProxyMock(with: .init(displayName: "Test", isDirect: true, isPublic: false, members: mockedMembers))
viewModel = RoomDetailsScreenViewModel(roomProxy: roomProxyMock, mediaProvider: MockMediaProvider())
_ = await context.$viewState.values.first(where: { !$0.members.isEmpty })
XCTAssertFalse(context.viewState.canEdit)
}
} }