Fixes #978 - Message forwarding support

* Add support for message forwarding

* Move the TimelineProvider within the RoomProxy to avoid it going out of sync and have it automatically register for timeline updates on init

* Implement single selection form button style for message forwarding

* Add searching and filter out the source room from the list

* Fixed emoji screen initial search bar height

* Add view models tests

* Add changelog

* Update strings

* Tweaks following code review
This commit is contained in:
Stefan Ceriu
2023-06-21 12:10:14 +03:00
committed by GitHub
parent f624add018
commit 2ae8f471c6
36 changed files with 800 additions and 140 deletions
+44
View File
@@ -112,6 +112,7 @@
2AD59AD5B09498EF8B3B04EC /* InvitesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */; };
2BA59D0AEFB4B82A2EC2A326 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; };
2BAA5B222856068158D0B3C6 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = B1E8B697DF78FE7F61FC6CA4 /* MatrixRustSDK */; };
2BBA132149DEBED6624084A8 /* MessageForwardingScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */; };
2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; };
2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; };
2CA6ABBC9A88EB89EA52FCCB /* ConfettiScene.scn in Resources */ = {isa = PBXBuildFile; fileRef = B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */; };
@@ -253,6 +254,7 @@
65EDA77363BEDC40CDE43B43 /* InvitesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ADEA322D2089391E049535 /* InvitesScreen.swift */; };
661A664C6EDF856B05519206 /* FilePreviewScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = F562E2CBA002E8E1B6545C38 /* FilePreviewScreen.swift */; };
663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; };
6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */; };
67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; };
67C05C50AD734283374605E3 /* MatrixEntityRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AD1A853D605C2146B0DC028 /* MatrixEntityRegex.swift */; };
67D6E0700A9C1E676F6231F8 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = AD544C0FA48DFFB080920061 /* Collections */; };
@@ -260,6 +262,7 @@
6832733838C57A7D3FE8FEB5 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 78A5A8DE1E2B09C978C7F3B0 /* KeychainAccess */; };
6860721DB3091BE08164C132 /* MapAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B48B7AD4908C5C374517B892 /* MapAssets.xcassets */; };
68AC3C84E2B438036B174E30 /* EmoteRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */; };
695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */; };
69BCBB4FB2DC3D61A28D3FD8 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */; };
69C7B956B74BEC3DB88224EA /* NavigationSplitCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */; };
6A0E7551E0D1793245F34CDD /* ClientError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D09A267106B9585D3D0CFC0D /* ClientError.swift */; };
@@ -517,6 +520,7 @@
BF675964C9159F718589C36A /* AnalyticsSettingsScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */; };
C051475DFF4C8EBDDF4DC8E4 /* StartChatScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B99E13633862847D8B7E2815 /* StartChatScreenModels.swift */; };
C08AAE7563E0722C9383F51C /* RoomMembersListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B8E176484A89BAC389D4076 /* RoomMembersListScreen.swift */; };
C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */; };
C1910A16BDF131FECA77BE22 /* EmojiPickerScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */; };
C1A5C386319835FB0C77736B /* ReportContentScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */; };
C1D0AB8222D7BAFC9AF9C8C0 /* MapLibreMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */; };
@@ -542,6 +546,7 @@
C76892321558E75101E68ED6 /* ReadableFrameModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */; };
C7CFDB4929DDD9A3B5BA085D /* BugReportViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB7ED3A898B07976F3AA90F /* BugReportViewModelTests.swift */; };
C8BD80891BAD688EF2C15CDB /* MediaUploadPreviewScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DD0855F2F76D47E5555082 /* MediaUploadPreviewScreenCoordinator.swift */; };
C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */; };
C9BE065FA7D4E77E4C61CB69 /* MapLibreModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = B81B6170DB690013CEB646F4 /* MapLibreModels.swift */; };
CA45758F08DF42D41D8A4B29 /* FilePreviewViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF38B69D2C331A499276F400 /* FilePreviewViewModelTests.swift */; };
CAF8755E152204F55F8D6B5B /* RoomMembersListViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */; };
@@ -657,11 +662,13 @@
F4433EF57B4BB3C077F8B00E /* SessionVerificationScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADD9E0FFA29EAACFF3AB9732 /* SessionVerificationScreenViewModel.swift */; };
F508683B76EF7B23BB2CBD6D /* TimelineItemPlainStylerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94BCC8A9C73C1F838122C645 /* TimelineItemPlainStylerView.swift */; };
F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; };
F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; };
F587A9AF25A262DE5A7B0369 /* ProgressTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F28551E81CE3700E5F1EC9B5 /* ProgressTracker.swift */; };
F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; };
F656F92A63D3DC1978D79427 /* Compound in Frameworks */ = {isa = PBXBuildFile; productRef = 07FEEEDB11543A7DED420F04 /* Compound */; };
F6F49E37272AD7397CD29A01 /* HomeScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 505208F28007C0FEC14E1FF0 /* HomeScreenViewModelTests.swift */; };
F7567DD6635434E8C563BF85 /* AnalyticsClientProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3B97591B2D3D4D67553506D /* AnalyticsClientProtocol.swift */; };
F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */; };
F78BAD28482A467287A9A5A3 /* EventBasedMessageTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0900BBF0A5D5D775E917C70 /* EventBasedMessageTimelineItemProtocol.swift */; };
F7BC744FFA7FE248FAE7F570 /* UserIndicatorToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */; };
F86102DC2C68BBBB0521BAAE /* SoftLogoutScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB385E148DE55C85C0A02D6 /* SoftLogoutScreenModels.swift */; };
@@ -752,6 +759,7 @@
05F598B1B346DAF223651C91 /* LoginScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreenCoordinator.swift; sourceTree = "<group>"; };
0685156EB62D7E243F097CFC /* ServerSelectionScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionScreenViewModelProtocol.swift; sourceTree = "<group>"; };
06B098A612DCB5A7358EECD5 /* DeveloperOptionsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenModels.swift; sourceTree = "<group>"; };
06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenCoordinator.swift; sourceTree = "<group>"; };
077D7C3BE199B6E5DDEC07EC /* AppCoordinatorStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorStateMachine.swift; sourceTree = "<group>"; };
07E65E613F057697A1A0BC03 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
086B997409328F091EBA43CE /* RoomScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreenUITests.swift; sourceTree = "<group>"; };
@@ -937,6 +945,7 @@
50E31AB0E77BB70E2BC77463 /* MatrixUserShareLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatrixUserShareLink.swift; sourceTree = "<group>"; };
51C2BCE0BC1FC69C1B36E688 /* BugReportScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenModels.swift; sourceTree = "<group>"; };
51C454AE59914B551A6D02C0 /* UserProfileProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileProxy.swift; sourceTree = "<group>"; };
52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenModels.swift; sourceTree = "<group>"; };
5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomScreen.swift; sourceTree = "<group>"; };
52BD6ED18E2EB61E28C340AD /* AttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedString.swift; sourceTree = "<group>"; };
52D7074991B3267B26D89B22 /* MockRoomTimelineController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineController.swift; sourceTree = "<group>"; };
@@ -957,6 +966,7 @@
584A61D9C459FAFEF038A7C0 /* Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
58C2527813FDAE23E72A9063 /* AnalyticsSettingsScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelTests.swift; sourceTree = "<group>"; };
592A35163B0749C66BFD6186 /* MapLibreStaticMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreStaticMapView.swift; sourceTree = "<group>"; };
59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenUITests.swift; sourceTree = "<group>"; };
5AEA0B743847CFA5B3C38EE4 /* RoomMembersListScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenCoordinator.swift; sourceTree = "<group>"; };
5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenCoordinator.swift; sourceTree = "<group>"; };
5C7C7CFA6B2A62A685FF6CE3 /* DeveloperOptionsScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreenCoordinator.swift; sourceTree = "<group>"; };
@@ -1007,6 +1017,7 @@
6E5E9C044BEB7C70B1378E91 /* UserSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSession.swift; sourceTree = "<group>"; };
6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindableState.swift; sourceTree = "<group>"; };
6F3DFE5B444F131648066F05 /* StateStoreViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateStoreViewModel.swift; sourceTree = "<group>"; };
6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelTests.swift; sourceTree = "<group>"; };
6FB31A32C93D94930B253FBF /* PermalinkBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermalinkBuilderTests.swift; sourceTree = "<group>"; };
6FC5015B9634698BDB8701AF /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemDebugView.swift; sourceTree = "<group>"; };
@@ -1196,6 +1207,7 @@
BEA38B9851CFCC4D67F5587D /* EmojiPickerScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiPickerScreenCoordinator.swift; sourceTree = "<group>"; };
BEBA759D1347CFFB3D84ED1F /* UserSessionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSessionStoreProtocol.swift; sourceTree = "<group>"; };
BFDCAC6CAAD65A2C24EA9C4B /* Dictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dictionary.swift; sourceTree = "<group>"; };
BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreen.swift; sourceTree = "<group>"; };
C024C151639C4E1B91FCC68B /* ElementXAttributeScope.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementXAttributeScope.swift; sourceTree = "<group>"; };
C070FD43DC6BF4E50217965A /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = "<group>"; };
C08E9043618AE5B0BF7B07E1 /* TemplateScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreenViewModelTests.swift; sourceTree = "<group>"; };
@@ -1264,12 +1276,14 @@
D6DC38E64A5ED3FDB201029A /* BugReportService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportService.swift; sourceTree = "<group>"; };
D77B3D4950F1707E66E4A45A /* AnalyticsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsConfiguration.swift; sourceTree = "<group>"; };
D8E057FB1F07A5C201C89061 /* MockServerSelectionScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockServerSelectionScreenState.swift; sourceTree = "<group>"; };
D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModel.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>"; };
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>"; };
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>"; };
DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageForwardingScreenViewModelProtocol.swift; sourceTree = "<group>"; };
DE846DDA83BFD7EC5C03760B /* ServerConfirmationScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerConfirmationScreenUITests.swift; sourceTree = "<group>"; };
DEC1D382565A4E9CAC2F14EA /* MediaFileHandleProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaFileHandleProxy.swift; sourceTree = "<group>"; };
DF05DA24F71B455E8EFEBC3B /* SessionVerificationViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationViewModelTests.swift; sourceTree = "<group>"; };
@@ -1754,6 +1768,18 @@
path = Views;
sourceTree = "<group>";
};
3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */ = {
isa = PBXGroup;
children = (
06FAE373A7F20780BA84B59C /* MessageForwardingScreenCoordinator.swift */,
52135BD9E0E7A091688F627A /* MessageForwardingScreenModels.swift */,
D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */,
DC10CCC8D68B863E20660DBC /* MessageForwardingScreenViewModelProtocol.swift */,
E816FC2BB467DD0B041C5A03 /* View */,
);
path = MessageForwardingScreen;
sourceTree = "<group>";
};
337015ADFBA3AB96660DB3A6 /* Generated */ = {
isa = PBXGroup;
children = (
@@ -2313,6 +2339,7 @@
F31F59030205A6F65B057E1A /* MatrixEntityRegexTests.swift */,
AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */,
03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */,
6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */,
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */,
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */,
9C698E30698EC59302A8EEBD /* NavigationStackCoordinatorTests.swift */,
@@ -2646,6 +2673,7 @@
ACCC1874C122E2BBE648B8F5 /* LegalInformationScreenUITests.swift */,
1DB34B0C74CD242FED9DD069 /* LoginScreenUITests.swift */,
39B6C8690AEA1E49FF1BAF95 /* MediaUploadPreviewScreenUITests.swift */,
59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */,
0C88046D6A070D9827181C4D /* OnboardingUITests.swift */,
4132F882A984ED971338EE9D /* ReportContentScreenUITests.swift */,
122186B7CD1BC46A9C629DD9 /* RoomDetailsEditScreenUITests.swift */,
@@ -3165,6 +3193,7 @@
948DD12A5533BE1BC260E437 /* LocationSharing */,
87E2774157D9C4894BCFF3F8 /* MediaPickerScreen */,
23605DD08620BE6558242469 /* MediaUploadPreviewScreen */,
3348D14DBDB54E72FC67E2F3 /* MessageForwardingScreen */,
3F38EAC92E2281990E65DAF2 /* OnboardingScreen */,
A448A3A8F764174C60CD0CA1 /* Other */,
5970F275D6014548DCED6106 /* ReportContentScreen */,
@@ -3251,6 +3280,14 @@
path = Authentication;
sourceTree = "<group>";
};
E816FC2BB467DD0B041C5A03 /* View */ = {
isa = PBXGroup;
children = (
BFEA446F8618DBA79A9239CC /* MessageForwardingScreen.swift */,
);
path = View;
sourceTree = "<group>";
};
EBBEB5471737E9D116DF4738 /* Background */ = {
isa = PBXGroup;
children = (
@@ -3804,6 +3841,7 @@
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */,
B9A8C34A00D03094C0CF56F3 /* MediaUploadPreviewScreenViewModelTests.swift in Sources */,
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */,
F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */,
4E8F17EBA24FBBA6ABB62ECB /* MockBackgroundTaskService.swift in Sources */,
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */,
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
@@ -4072,6 +4110,11 @@
8A0BD60CA4A6004DB06B5403 /* MediaUploadingPreprocessor.swift in Sources */,
24906A1E82D0046655958536 /* MessageComposer.swift in Sources */,
072BA9DBA932374CCA300125 /* MessageComposerTextField.swift in Sources */,
C8E0FA0FF2CD6613264FA6B9 /* MessageForwardingScreen.swift in Sources */,
2BBA132149DEBED6624084A8 /* MessageForwardingScreenCoordinator.swift in Sources */,
695825D20A761C678809345D /* MessageForwardingScreenModels.swift in Sources */,
F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */,
C13128AAA787A4C2CBE4EE82 /* MessageForwardingScreenViewModelProtocol.swift in Sources */,
152AE2B8650FB23AFD2E28B9 /* MockAuthenticationServiceProxy.swift in Sources */,
EE4F5601356228FF72FC56B6 /* MockClientProxy.swift in Sources */,
B659E3A49889E749E3239EA7 /* MockMediaProvider.swift in Sources */,
@@ -4335,6 +4378,7 @@
8868ACFA45E5B5E19514B575 /* LegalInformationScreenUITests.swift in Sources */,
5C8AFBF168A41E20835F3B86 /* LoginScreenUITests.swift in Sources */,
7FB0BDE26838F1A92782D5E1 /* MediaUploadPreviewScreenUITests.swift in Sources */,
6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */,
6B15FF984906AAFCF9DC4F58 /* OnboardingUITests.swift in Sources */,
BA0D3DDCEDD97502DAC4B6E9 /* ReportContentScreenUITests.swift in Sources */,
F16109A6F6DF03DA26D59233 /* RoomDetailsEditScreenUITests.swift in Sources */,
@@ -192,15 +192,16 @@
"screen_account_provider_form_notice" = "Enter a search term or a domain address.";
"screen_account_provider_form_subtitle" = "Search for a company, community, or private server.";
"screen_account_provider_form_title" = "Find an account provider";
"screen_account_provider_signin_subtitle" = "This is where you conversations will live — just like you would use an email provider to keep your emails.";
"screen_account_provider_signin_title" = "You’re about to sign in to %@";
"screen_account_provider_signup_subtitle" = "This is where you conversations will live — just like you would use an email provider to keep your emails.";
"screen_account_provider_signup_title" = "You’re about to create an account on %@";
"screen_analytics_prompt_data_usage" = "We <b>don't</b> record or profile any account data";
"screen_analytics_prompt_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
"screen_analytics_prompt_data_usage" = "We won't record or profile any personal data";
"screen_analytics_prompt_help_us_improve" = "Share anonymous usage data to help us identify issues.";
"screen_analytics_prompt_read_terms" = "You can read all our terms %1$@.";
"screen_analytics_prompt_read_terms_content_link" = "here";
"screen_analytics_prompt_settings" = "You can turn this off anytime in settings";
"screen_analytics_prompt_third_party_sharing" = "We <b>don't</b> share information with third parties";
"screen_analytics_prompt_settings" = "You can turn this off anytime";
"screen_analytics_prompt_third_party_sharing" = "We won't share your data with third parties";
"screen_analytics_prompt_title" = "Help improve %1$@";
"screen_analytics_settings_share_data" = "Share analytics data";
"screen_bug_report_attach_screenshot" = "Attach screenshot";
@@ -223,6 +224,7 @@
"screen_change_server_form_notice" = "You can only connect to an existing server that supports sliding sync. Your homeserver admin will need to configure it. %1$@";
"screen_change_server_subtitle" = "What is the address of your server?";
"screen_create_room_action_create_room" = "New room";
"screen_create_room_action_invite_people" = "Invite friends to Element";
"screen_create_room_add_people_title" = "Invite people";
"screen_create_room_error_creating_room" = "An error occurred when creating the room";
"screen_create_room_private_option_description" = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.";
@@ -269,6 +271,7 @@
"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_invite_people_title" = "Invite people";
"screen_room_details_notification_title" = "Notification";
"screen_room_details_room_name_label" = "Room name";
"screen_room_details_share_room_title" = "Share room";
"screen_room_details_updating_room" = "Updating room…";
@@ -283,6 +286,7 @@
"screen_room_member_details_unblock_user" = "Unblock user";
"screen_room_member_list_pending_header_title" = "Pending";
"screen_room_member_list_room_members_header_title" = "Room members";
"screen_room_no_permission_to_post" = "You do not have permission to post to this room";
"screen_room_retry_send_menu_send_again_action" = "Send again";
"screen_room_retry_send_menu_title" = "Your message failed to send";
"screen_roomlist_a11y_create_message" = "Create a new conversation or room";
@@ -377,7 +381,7 @@
"dialog_title_success" = "Success";
"notification_room_action_quick_reply" = "Quick reply";
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
"screen_analytics_settings_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
"screen_analytics_settings_help_us_improve" = "Share anonymous usage data to help us identify issues.";
"screen_analytics_settings_read_terms" = "You can read all our terms %1$@.";
"screen_analytics_settings_read_terms_content_link" = "here";
"screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
@@ -29,6 +29,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
private let navigationStackCoordinator: NavigationStackCoordinator
private let navigationSplitCoordinator: NavigationSplitCoordinator
private let emojiProvider: EmojiProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let stateMachine: StateMachine<State, Event> = .init(state: .initial)
@@ -46,12 +47,14 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol,
navigationStackCoordinator: NavigationStackCoordinator,
navigationSplitCoordinator: NavigationSplitCoordinator,
emojiProvider: EmojiProviderProtocol) {
emojiProvider: EmojiProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol) {
self.userSession = userSession
self.roomTimelineControllerFactory = roomTimelineControllerFactory
self.navigationStackCoordinator = navigationStackCoordinator
self.navigationSplitCoordinator = navigationSplitCoordinator
self.emojiProvider = emojiProvider
self.userIndicatorController = userIndicatorController
setupStateMachine()
}
@@ -137,6 +140,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.dismissEmojiPicker, .emojiPicker(let roomID, _)):
return .room(roomID: roomID)
case (.presentMessageForwarding(let itemID), .room(let roomID)):
return .messageForwarding(roomID: roomID, itemID: itemID)
case (.dismissMessageForwarding, .messageForwarding(let roomID, _)):
return .room(roomID: roomID)
default:
return nil
}
@@ -152,7 +160,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
dismissRoom(animated: animated)
presentRoom(roomID, animated: animated)
case (_, .presentRoom(let roomID), .room):
presentRoom(roomID, animated: animated)
let destinationRoomProxy = (context.userInfo as? EventUserInfo)?.destinationRoomProxy
presentRoom(roomID, animated: animated, destinationRoomProxy: destinationRoomProxy)
case (.room, .dismissRoom, .initial):
dismissRoom(animated: animated)
@@ -199,6 +208,11 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case (.roomMemberDetails, .dismissRoomMemberDetails, .room):
break
case (.room, .presentMessageForwarding(let eventID), .messageForwarding):
presentMessageForwarding(for: eventID)
case (.messageForwarding, .dismissMessageForwarding, .room):
break
default:
fatalError("Unknown transition: \(context)")
}
@@ -217,23 +231,39 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
}
}
private func presentRoom(_ roomID: String, animated: Bool) {
/// Updates the navigation stack so it displays the timeline for the given room
/// - Parameters:
/// - roomID: the identifier of the room that is to be presented
/// - animated: whether it should animate the transition
/// - destinationRoomProxy: an optional already build roomProxy for the target room. It is currently used when
/// forwarding messages so that we can take advantage of the local echo
/// and have the message already there when presenting the room
private func presentRoom(_ roomID: String, animated: Bool, destinationRoomProxy: RoomProxyProtocol? = nil) {
Task {
await asyncPresentRoom(roomID, animated: animated)
await asyncPresentRoom(roomID, animated: animated, destinationRoomProxy: destinationRoomProxy)
}
}
// swiftlint:disable:next cyclomatic_complexity function_body_length
private func asyncPresentRoom(_ roomID: String, animated: Bool) async {
private func asyncPresentRoom(_ roomID: String, animated: Bool, destinationRoomProxy: RoomProxyProtocol? = nil) async {
if let roomProxy, roomProxy.id == roomID {
navigationStackCoordinator.popToRoot()
return
}
guard let roomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Invalid room identifier: \(roomID)")
stateMachine.tryEvent(.dismissRoom)
return
let roomProxy: RoomProxyProtocol
if let destinationRoomProxy {
roomProxy = destinationRoomProxy
} else {
guard let proxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Invalid room identifier: \(roomID)")
stateMachine.tryEvent(.dismissRoom)
return
}
roomProxy = proxy
}
actionsSubject.send(.presentedRoom(roomID))
@@ -277,6 +307,8 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
stateMachine.tryEvent(.presentEmojiPicker(itemID: itemID))
case .presentRoomMemberDetails(member: let member):
stateMachine.tryEvent(.presentRoomMemberDetails(member: .init(value: member)))
case .presentMessageForwarding(let itemID):
stateMachine.tryEvent(.presentMessageForwarding(itemID: itemID))
}
}
.store(in: &cancellables)
@@ -398,7 +430,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
case .cancel:
break
case .finish:
self?.showSuccess(label: L10n.commonReportSubmitted)
userIndicatorController.submitIndicator(UserIndicator(title: L10n.commonReportSubmitted, iconName: "checkmark"))
}
}
navigationCoordinator.setRootCoordinator(coordinator)
@@ -492,9 +524,65 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
self?.stateMachine.tryEvent(.dismissRoomMemberDetails)
}
}
private func presentMessageForwarding(for eventID: String) {
guard let roomProxy, let roomSummaryProvider = userSession.clientProxy.roomSummaryProvider else {
fatalError()
}
let messageForwardingNavigationStackCoordinator = NavigationStackCoordinator()
let parameters = MessageForwardingScreenCoordinatorParameters(roomSummaryProvider: roomSummaryProvider,
sourceRoomID: roomProxy.id)
let coordinator = MessageForwardingScreenCoordinator(parameters: parameters)
coordinator.actions.sink { [weak self] action in
guard let self else { return }
switch action {
case .dismiss:
navigationStackCoordinator.setSheetCoordinator(nil)
case .send(let roomID):
navigationStackCoordinator.setSheetCoordinator(nil)
Task {
await self.forward(eventID: eventID, toRoomID: roomID)
}
}
}.store(in: &cancellables)
messageForwardingNavigationStackCoordinator.setRootCoordinator(coordinator)
private func showSuccess(label: String) {
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(title: label, iconName: "checkmark"))
navigationStackCoordinator.setSheetCoordinator(messageForwardingNavigationStackCoordinator) { [weak self] in
self?.stateMachine.tryEvent(.dismissMessageForwarding)
}
}
private func forward(eventID: String, toRoomID roomID: String) async {
guard let roomProxy else {
MXLog.error("Failed retrieving current room with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
guard let messageEventContent = roomProxy.messageEventContent(for: eventID) else {
MXLog.error("Failed retrieving forwarded message event content for eventID: \(eventID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
guard let targetRoomProxy = await userSession.clientProxy.roomForIdentifier(roomID) else {
MXLog.error("Failed retrieving room to forward to with id: \(roomID)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
if case .failure(let error) = await targetRoomProxy.sendMessageEventContent(messageEventContent) {
MXLog.error("Failed forwarding message with error: \(error)")
userIndicatorController.submitIndicator(UserIndicator(title: L10n.errorUnknown))
return
}
stateMachine.tryEvent(.presentRoom(roomID: roomID), userInfo: EventUserInfo(animated: true, destinationRoomProxy: targetRoomProxy))
}
}
@@ -521,10 +609,12 @@ private extension RoomFlowCoordinator {
case mediaUploadPreview(roomID: String, fileURL: URL)
case emojiPicker(roomID: String, itemID: String)
case roomMemberDetails(roomID: String, member: HashableRoomMemberWrapper)
case messageForwarding(roomID: String, itemID: String)
}
struct EventUserInfo {
let animated: Bool
var destinationRoomProxy: RoomProxyProtocol?
}
enum Event: EventType {
@@ -551,5 +641,8 @@ private extension RoomFlowCoordinator {
case presentRoomMemberDetails(member: HashableRoomMemberWrapper)
case dismissRoomMemberDetails
case presentMessageForwarding(itemID: String)
case dismissMessageForwarding
}
}
@@ -59,7 +59,8 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
roomTimelineControllerFactory: roomTimelineControllerFactory,
navigationStackCoordinator: detailNavigationStackCoordinator,
navigationSplitCoordinator: navigationSplitCoordinator,
emojiProvider: EmojiProvider())
emojiProvider: EmojiProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
setupStateMachine()
+15 -11
View File
@@ -466,6 +466,8 @@ public enum L10n {
public static var screenAccountProviderFormSubtitle: String { return L10n.tr("Localizable", "screen_account_provider_form_subtitle") }
/// Find an account provider
public static var screenAccountProviderFormTitle: String { return L10n.tr("Localizable", "screen_account_provider_form_title") }
/// This is where you conversations will live — just like you would use an email provider to keep your emails.
public static var screenAccountProviderSigninSubtitle: String { return L10n.tr("Localizable", "screen_account_provider_signin_subtitle") }
/// You’re about to sign in to %@
public static func screenAccountProviderSigninTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_account_provider_signin_title", String(describing: p1))
@@ -476,30 +478,26 @@ public enum L10n {
public static func screenAccountProviderSignupTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_account_provider_signup_title", String(describing: p1))
}
/// We <b>don't</b> record or profile any account data
/// We won't record or profile any personal data
public static var screenAnalyticsPromptDataUsage: String { return L10n.tr("Localizable", "screen_analytics_prompt_data_usage") }
/// Help us identify issues and improve %1$@ by sharing anonymous usage data.
public static func screenAnalyticsPromptHelpUsImprove(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_analytics_prompt_help_us_improve", String(describing: p1))
}
/// Share anonymous usage data to help us identify issues.
public static var screenAnalyticsPromptHelpUsImprove: String { return L10n.tr("Localizable", "screen_analytics_prompt_help_us_improve") }
/// You can read all our terms %1$@.
public static func screenAnalyticsPromptReadTerms(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_analytics_prompt_read_terms", String(describing: p1))
}
/// here
public static var screenAnalyticsPromptReadTermsContentLink: String { return L10n.tr("Localizable", "screen_analytics_prompt_read_terms_content_link") }
/// You can turn this off anytime in settings
/// You can turn this off anytime
public static var screenAnalyticsPromptSettings: String { return L10n.tr("Localizable", "screen_analytics_prompt_settings") }
/// We <b>don't</b> share information with third parties
/// We won't share your data with third parties
public static var screenAnalyticsPromptThirdPartySharing: String { return L10n.tr("Localizable", "screen_analytics_prompt_third_party_sharing") }
/// Help improve %1$@
public static func screenAnalyticsPromptTitle(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_analytics_prompt_title", String(describing: p1))
}
/// Help us identify issues and improve %1$@ by sharing anonymous usage data.
public static func screenAnalyticsSettingsHelpUsImprove(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_analytics_settings_help_us_improve", String(describing: p1))
}
/// Share anonymous usage data to help us identify issues.
public static var screenAnalyticsSettingsHelpUsImprove: String { return L10n.tr("Localizable", "screen_analytics_settings_help_us_improve") }
/// You can read all our terms %1$@.
public static func screenAnalyticsSettingsReadTerms(_ p1: Any) -> String {
return L10n.tr("Localizable", "screen_analytics_settings_read_terms", String(describing: p1))
@@ -558,6 +556,8 @@ public enum L10n {
public static var screenChangeServerTitle: String { return L10n.tr("Localizable", "screen_change_server_title") }
/// New room
public static var screenCreateRoomActionCreateRoom: String { return L10n.tr("Localizable", "screen_create_room_action_create_room") }
/// Invite friends to Element
public static var screenCreateRoomActionInvitePeople: String { return L10n.tr("Localizable", "screen_create_room_action_invite_people") }
/// Invite people
public static var screenCreateRoomAddPeopleTitle: String { return L10n.tr("Localizable", "screen_create_room_add_people_title") }
/// An error occurred when creating the room
@@ -684,6 +684,8 @@ public enum L10n {
public static var screenRoomDetailsInvitePeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_invite_people_title") }
/// Leave room
public static var screenRoomDetailsLeaveRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_leave_room_title") }
/// Notification
public static var screenRoomDetailsNotificationTitle: String { return L10n.tr("Localizable", "screen_room_details_notification_title") }
/// People
public static var screenRoomDetailsPeopleTitle: String { return L10n.tr("Localizable", "screen_room_details_people_title") }
/// Room name
@@ -724,6 +726,8 @@ public enum L10n {
public static var screenRoomMemberListPendingHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_pending_header_title") }
/// Room members
public static var screenRoomMemberListRoomMembersHeaderTitle: String { return L10n.tr("Localizable", "screen_room_member_list_room_members_header_title") }
/// You do not have permission to post to this room
public static var screenRoomNoPermissionToPost: String { return L10n.tr("Localizable", "screen_room_no_permission_to_post") }
/// Remove
public static var screenRoomRetrySendMenuRemoveAction: String { return L10n.tr("Localizable", "screen_room_retry_send_menu_remove_action") }
/// Send again
@@ -457,6 +457,11 @@ class RoomProxyMock: RoomProxyProtocol {
set(value) { underlyingUpdatesPublisher = value }
}
var underlyingUpdatesPublisher: AnyPublisher<TimelineDiff, Never>!
var timelineProvider: RoomTimelineProviderProtocol {
get { return underlyingTimelineProvider }
set(value) { underlyingTimelineProvider = value }
}
var underlyingTimelineProvider: RoomTimelineProviderProtocol!
//MARK: - loadAvatarURLForUserId
@@ -500,23 +505,6 @@ class RoomProxyMock: RoomProxyProtocol {
return loadDisplayNameForUserIdReturnValue
}
}
//MARK: - registerTimelineListenerIfNeeded
var registerTimelineListenerIfNeededCallsCount = 0
var registerTimelineListenerIfNeededCalled: Bool {
return registerTimelineListenerIfNeededCallsCount > 0
}
var registerTimelineListenerIfNeededReturnValue: Result<[TimelineItem], RoomProxyError>!
var registerTimelineListenerIfNeededClosure: (() -> Result<[TimelineItem], RoomProxyError>)?
func registerTimelineListenerIfNeeded() -> Result<[TimelineItem], RoomProxyError> {
registerTimelineListenerIfNeededCallsCount += 1
if let registerTimelineListenerIfNeededClosure = registerTimelineListenerIfNeededClosure {
return registerTimelineListenerIfNeededClosure()
} else {
return registerTimelineListenerIfNeededReturnValue
}
}
//MARK: - paginateBackwards
var paginateBackwardsRequestSizeUntilNumberOfItemsCallsCount = 0
@@ -559,6 +547,48 @@ class RoomProxyMock: RoomProxyProtocol {
return sendReadReceiptForReturnValue
}
}
//MARK: - messageEventContent
var messageEventContentForCallsCount = 0
var messageEventContentForCalled: Bool {
return messageEventContentForCallsCount > 0
}
var messageEventContentForReceivedEventID: String?
var messageEventContentForReceivedInvocations: [String] = []
var messageEventContentForReturnValue: RoomMessageEventContent?
var messageEventContentForClosure: ((String) -> RoomMessageEventContent?)?
func messageEventContent(for eventID: String) -> RoomMessageEventContent? {
messageEventContentForCallsCount += 1
messageEventContentForReceivedEventID = eventID
messageEventContentForReceivedInvocations.append(eventID)
if let messageEventContentForClosure = messageEventContentForClosure {
return messageEventContentForClosure(eventID)
} else {
return messageEventContentForReturnValue
}
}
//MARK: - sendMessageEventContent
var sendMessageEventContentCallsCount = 0
var sendMessageEventContentCalled: Bool {
return sendMessageEventContentCallsCount > 0
}
var sendMessageEventContentReceivedMessageContent: RoomMessageEventContent?
var sendMessageEventContentReceivedInvocations: [RoomMessageEventContent] = []
var sendMessageEventContentReturnValue: Result<Void, RoomProxyError>!
var sendMessageEventContentClosure: ((RoomMessageEventContent) async -> Result<Void, RoomProxyError>)?
func sendMessageEventContent(_ messageContent: RoomMessageEventContent) async -> Result<Void, RoomProxyError> {
sendMessageEventContentCallsCount += 1
sendMessageEventContentReceivedMessageContent = messageContent
sendMessageEventContentReceivedInvocations.append(messageContent)
if let sendMessageEventContentClosure = sendMessageEventContentClosure {
return await sendMessageEventContentClosure(messageContent)
} else {
return sendMessageEventContentReturnValue
}
}
//MARK: - sendMessage
var sendMessageInReplyToCallsCount = 0
@@ -74,7 +74,6 @@ extension RoomProxyMock {
updateMembersClosure = { }
acceptInvitationClosure = { .success(()) }
registerTimelineListenerIfNeededClosure = { .success([]) }
underlyingUpdatesPublisher = Empty(completeImmediately: false).eraseToAnyPublisher()
setNameClosure = { _ in .success(()) }
setTopicClosure = { _ in .success(()) }
+3
View File
@@ -76,12 +76,15 @@ enum UserAvatarSizeOnScreen {
enum RoomAvatarSizeOnScreen {
case timeline
case home
case messageForwarding
case details
var value: CGFloat {
switch self {
case .timeline:
return 32
case .messageForwarding:
return 36
case .home:
return 52
case .details:
@@ -23,7 +23,8 @@ struct FormRowAccessory: View {
enum Kind {
case navigationLink
case progressView
case selection(isSelected: Bool)
case singleSelection(isSelected: Bool)
case multipleSelection(isSelected: Bool)
}
let kind: Kind
@@ -36,8 +37,12 @@ struct FormRowAccessory: View {
.init(kind: .progressView)
}
static func selection(isSelected: Bool) -> Self {
.init(kind: .selection(isSelected: isSelected))
static func singleSelection(isSelected: Bool) -> Self {
.init(kind: .singleSelection(isSelected: isSelected))
}
static func multipleSelection(isSelected: Bool) -> Self {
.init(kind: .multipleSelection(isSelected: isSelected))
}
var body: some View {
@@ -48,7 +53,13 @@ struct FormRowAccessory: View {
.foregroundColor(.compound.iconQuaternary)
case .progressView:
ProgressView()
case .selection(let isSelected):
case .singleSelection(let isSelected):
if isSelected {
Image(systemName: "checkmark.circle.fill")
.font(.compound.bodyLG)
.foregroundColor(isSelected && isEnabled ? .compound.iconPrimary : .compound.iconTertiary)
}
case .multipleSelection(let isSelected):
Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
.font(.compound.bodyLG)
.foregroundColor(isSelected && isEnabled ? .compound.iconPrimary : .compound.iconTertiary)
@@ -94,7 +105,16 @@ struct FormButtonStyle: PrimitiveButtonStyle {
}
.contentShape(Rectangle())
.padding(FormRow.insets) // Re-apply the normal insets using padding.
.background(configuration.isPressed ? Color.compound.bgSubtlePrimary : .clear)
.background(backgroundColor(for: configuration))
}
private func backgroundColor(for configuration: Configuration) -> Color {
switch accessory?.kind {
case .none, .navigationLink, .progressView:
return configuration.isPressed ? .compound.bgSubtlePrimary : .element.formRowBackground
default:
return .element.formRowBackground
}
}
}
}
@@ -147,7 +167,7 @@ struct FormButtonStyles_Previews: PreviewProvider {
}
.formSectionStyle()
Section {
Section("Single selection") {
Button { } label: {
Text("Hello world")
}
@@ -156,18 +176,42 @@ struct FormButtonStyles_Previews: PreviewProvider {
Button { } label: {
Text("Selected")
}
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: true)))
Button { } label: {
Text("Selected (disabled)")
}
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: true)))
.disabled(true)
Button { } label: {
Text("Unselected")
}
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: false)))
}
.formSectionStyle()
Section("Multiple selection") {
Button { } label: {
Text("Hello world")
}
.buttonStyle(FormButtonStyle())
Button { } label: {
Text("Selected")
}
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: true)))
Button { } label: {
Text("Selected (disabled)")
}
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: true)))
.disabled(true)
Button { } label: {
Text("Unselected")
}
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: false)))
}
.formSectionStyle()
@@ -97,33 +97,33 @@ struct UserProfileCell_Previews: PreviewProvider {
Button(action: action) {
UserProfileCell(user: .mockAlice, membership: nil, imageProvider: MockMediaProvider())
}
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: true)))
.previewDisplayName("Selected user")
Button(action: action) {
UserProfileCell(user: .mockBob, membership: nil, imageProvider: MockMediaProvider())
}
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: false)))
.previewDisplayName("Unselected user")
Button(action: action) {
UserProfileCell(user: .mockCharlie, membership: .join, imageProvider: MockMediaProvider())
}
.disabled(true)
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: true)))
.previewDisplayName("Selected disabled user")
Button(action: action) {
UserProfileCell(user: .init(userID: "@someone:matrix.org"), membership: .join, imageProvider: MockMediaProvider())
}
.disabled(true)
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: true)))
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: true)))
.previewDisplayName("Unverified joined user")
Button(action: action) {
UserProfileCell(user: .init(userID: "@someone:matrix.org"), membership: nil, imageProvider: MockMediaProvider())
}
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: false)))
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: false)))
.previewDisplayName("Unverified user")
}
}
@@ -43,7 +43,7 @@ struct AnalyticsPromptScreenStrings {
let point3 = L10n.screenAnalyticsPromptSettings
init(termsURL: URL) {
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove)
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
@@ -45,7 +45,7 @@ struct EmojiPickerScreen: View {
.navigationTitle(L10n.commonReactions)
.navigationBarTitleDisplayMode(.inline)
.toolbar { toolbar }
.searchable(text: $searchString)
.searchable(text: $searchString, placement: .navigationBarDrawer(displayMode: .always))
.compoundSearchField()
.onChange(of: searchString) { _ in
context.send(viewAction: .search(searchString: searchString))
@@ -53,7 +53,7 @@ struct HomeScreenRoomCell: View {
.accessibilityIdentifier(A11yIdentifiers.homeScreen.roomName(room.name))
}
@ViewBuilder
@ViewBuilder @MainActor
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
LoadableAvatarImage(url: room.avatarURL,
@@ -79,7 +79,7 @@ struct InviteUsersScreen: View {
imageProvider: context.imageProvider)
}
.disabled(context.viewState.isUserDisabled(user))
.buttonStyle(FormButtonStyle(accessory: .selection(isSelected: context.viewState.isUserSelected(user))))
.buttonStyle(FormButtonStyle(accessory: .multipleSelection(isSelected: context.viewState.isUserSelected(user))))
}
} header: {
if let title = context.viewState.usersSection.title {
@@ -0,0 +1,62 @@
//
// 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 MessageForwardingScreenCoordinatorParameters {
let roomSummaryProvider: RoomSummaryProviderProtocol
let sourceRoomID: String
}
enum MessageForwardingScreenCoordinatorAction {
case dismiss
case send(roomID: String)
}
final class MessageForwardingScreenCoordinator: CoordinatorProtocol {
private let parameters: MessageForwardingScreenCoordinatorParameters
private var viewModel: MessageForwardingScreenViewModelProtocol
private let actionsSubject: PassthroughSubject<MessageForwardingScreenCoordinatorAction, Never> = .init()
private var cancellables: Set<AnyCancellable> = .init()
var actions: AnyPublisher<MessageForwardingScreenCoordinatorAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(parameters: MessageForwardingScreenCoordinatorParameters) {
self.parameters = parameters
viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: parameters.roomSummaryProvider,
sourceRoomID: parameters.sourceRoomID)
}
func start() {
viewModel.actions.sink { [weak self] action in
switch action {
case .dismiss:
self?.actionsSubject.send(.dismiss)
case .send(let roomID):
self?.actionsSubject.send(.send(roomID: roomID))
}
}
.store(in: &cancellables)
}
func toPresentable() -> AnyView {
AnyView(MessageForwardingScreen(context: viewModel.context))
}
}
@@ -0,0 +1,54 @@
//
// 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 MessageForwardingScreenViewModelAction {
case dismiss
case send(roomID: String)
}
struct MessageForwardingScreenViewState: BindableState {
var rooms: [MessageForwardingRoom] = []
var selectedRoomID: String?
var visibleRooms: [MessageForwardingRoom] {
if bindings.searchQuery.isEmpty {
return rooms
}
return rooms.filter { $0.name.localizedStandardContains(bindings.searchQuery) }
}
var bindings = MessageForwardingScreenViewStateBindings()
}
struct MessageForwardingScreenViewStateBindings {
var searchQuery = ""
}
enum MessageForwardingScreenViewAction {
case cancel
case send
case selectRoom(roomID: String)
}
struct MessageForwardingRoom: Identifiable, Equatable {
let id: String
let name: String
let alias: String?
let avatarURL: URL?
}
@@ -0,0 +1,94 @@
//
// 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 MessageForwardingScreenViewModelType = StateStoreViewModel<MessageForwardingScreenViewState, MessageForwardingScreenViewAction>
class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, MessageForwardingScreenViewModelProtocol {
private let roomSummaryProvider: RoomSummaryProviderProtocol?
private let sourceRoomID: String
private var actionsSubject: PassthroughSubject<MessageForwardingScreenViewModelAction, Never> = .init()
var actions: AnyPublisher<MessageForwardingScreenViewModelAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
init(roomSummaryProvider: RoomSummaryProviderProtocol,
sourceRoomID: String) {
self.roomSummaryProvider = roomSummaryProvider
self.sourceRoomID = sourceRoomID
super.init(initialViewState: MessageForwardingScreenViewState())
roomSummaryProvider.roomListPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.updateRooms()
}
.store(in: &cancellables)
updateRooms()
}
override func process(viewAction: MessageForwardingScreenViewAction) {
switch viewAction {
case .cancel:
actionsSubject.send(.dismiss)
case .send:
guard let roomID = state.selectedRoomID else {
fatalError()
}
actionsSubject.send(.send(roomID: roomID))
case .selectRoom(let roomID):
state.selectedRoomID = roomID
}
}
// MARK: - Private
private func updateRooms() {
guard let roomSummaryProvider else {
MXLog.error("Room summary provider unavailable")
return
}
MXLog.info("Updating rooms")
var rooms = [MessageForwardingRoom]()
for summary in roomSummaryProvider.roomListPublisher.value {
switch summary {
case .empty, .invalidated:
continue
case .filled(let details):
if details.id == sourceRoomID {
continue
}
let room = MessageForwardingRoom(id: details.id, name: details.name, alias: details.canonicalAlias, avatarURL: details.avatarURL)
rooms.append(room)
}
}
state.rooms = rooms
MXLog.info("Finished updating rooms")
}
}
@@ -0,0 +1,23 @@
//
// 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
@MainActor
protocol MessageForwardingScreenViewModelProtocol {
var actions: AnyPublisher<MessageForwardingScreenViewModelAction, Never> { get }
var context: MessageForwardingScreenViewModelType.Context { get }
}
@@ -0,0 +1,108 @@
//
// 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 MessageForwardingScreen: View {
@ObservedObject var context: MessageForwardingScreenViewModel.Context
var body: some View {
Form {
Section {
ForEach(context.viewState.visibleRooms) { room in
MessageForwardingRoomCell(room: room, context: context)
.buttonStyle(FormButtonStyle(accessory: .singleSelection(isSelected: context.viewState.selectedRoomID == room.id)))
}
}
.formSectionStyle()
}
.listRowSeparator(.automatic)
.elementFormStyle()
.navigationTitle(L10n.commonForwardMessage)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button(L10n.actionCancel) {
context.send(viewAction: .cancel)
}
}
ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionSend) {
context.send(viewAction: .send)
}
.disabled(context.viewState.selectedRoomID == nil)
}
}
.searchable(text: $context.searchQuery, placement: .navigationBarDrawer(displayMode: .always))
.compoundSearchField()
.disableAutocorrection(true)
}
}
private struct MessageForwardingRoomCell: View {
@Environment(\.dynamicTypeSize) var dynamicTypeSize
let room: MessageForwardingRoom
let context: MessageForwardingScreenViewModel.Context
var body: some View {
Button {
context.send(viewAction: .selectRoom(roomID: room.id))
} label: {
HStack(spacing: 16.0) {
avatar
VStack(alignment: .leading, spacing: 2.0) {
Text(room.name)
.font(.compound.bodyMD)
.foregroundColor(.compound.textPrimary)
.lineLimit(1)
Text(room.alias ?? room.id)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(1)
}
}
.accessibilityElement(children: .combine)
}
}
@ViewBuilder @MainActor
var avatar: some View {
if dynamicTypeSize < .accessibility3 {
LoadableAvatarImage(url: room.avatarURL,
name: room.name,
contentID: room.id,
avatarSize: .room(on: .messageForwarding),
imageProvider: context.imageProvider)
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
.accessibilityHidden(true)
}
}
}
// MARK: - Previews
struct MessageForwardingScreen_Previews: PreviewProvider {
static var previews: some View {
let summaryProvider = MockRoomSummaryProvider(state: .loaded(.mockRooms))
let viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: summaryProvider, sourceRoomID: "")
NavigationStack {
MessageForwardingScreen(context: viewModel.context)
}
}
}
@@ -89,13 +89,6 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
// MARK: - Private
private func setupRoomSubscription() {
switch roomProxy.registerTimelineListenerIfNeeded() {
case .success, .failure(.roomListenerAlreadyRegistered):
break
case .failure:
MXLog.error("Failed to register a room listener in room's details for the room \(roomProxy.id)")
}
roomProxy.updatesPublisher
.throttle(for: .milliseconds(200), scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
@@ -32,6 +32,7 @@ enum RoomScreenCoordinatorAction {
case presentRoomDetails
case presentEmojiPicker(itemID: String)
case presentRoomMemberDetails(member: RoomMemberProxyProtocol)
case presentMessageForwarding(itemID: String)
}
final class RoomScreenCoordinator: CoordinatorProtocol {
@@ -54,6 +55,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
// MARK: - Public
// swiftlint:disable:next cyclomatic_complexity
func start() {
viewModel.callback = { [weak self] action in
guard let self else { return }
@@ -77,6 +79,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
actionsSubject.send(.presentMediaUploadPreviewScreen(url))
case .displayRoomMemberDetails(let member):
actionsSubject.send(.presentRoomMemberDetails(member: member))
case .displayMessageForwarding(let itemID):
actionsSubject.send(.presentMessageForwarding(itemID: itemID))
}
}
}
@@ -28,6 +28,7 @@ enum RoomScreenViewModelAction {
case displayDocumentPicker
case displayMediaUploadPreviewScreen(url: URL)
case displayRoomMemberDetails(member: RoomMemberProxyProtocol)
case displayMessageForwarding(itemID: String)
}
enum RoomScreenComposerMode: Equatable {
@@ -330,12 +330,14 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
}
var actions: [TimelineItemMenuAction] = [
.reply, .copyPermalink
.reply
]
if timelineItem is EventBasedMessageTimelineItemProtocol {
actions.append(contentsOf: [.copy, .quote])
actions.append(contentsOf: [.forward(itemID: itemId), .copy, .quote])
}
actions.append(.copyPermalink)
if item.isEditable {
actions.append(.edit)
@@ -403,6 +405,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
let replyDetails = TimelineItemReplyDetails.loaded(sender: eventTimelineItem.sender, contentType: buildReplyContent(for: eventTimelineItem))
state.composerMode = .reply(itemID: eventTimelineItem.id, replyDetails: replyDetails)
case .forward(let itemID):
callback?(.displayMessageForwarding(itemID: itemID))
case .viewSource:
let debugInfo = timelineController.debugInfo(for: eventTimelineItem.id)
MXLog.info(debugInfo)
@@ -37,6 +37,7 @@ enum TimelineItemMenuAction: Identifiable, Hashable {
case copyPermalink
case redact
case reply
case forward(itemID: String)
case viewSource
case retryDecryption(sessionID: String)
case report
@@ -193,6 +194,10 @@ public struct TimelineItemMenu: View {
Button { send(action) } label: {
MenuLabel(title: L10n.actionReply, systemImageName: "arrowshape.turn.up.left")
}
case .forward:
Button { send(action) } label: {
MenuLabel(title: L10n.actionForward, systemImageName: "arrowshape.turn.up.right")
}
case .redact:
Button(role: .destructive) { send(action) } label: {
MenuLabel(title: L10n.actionRemove, systemImageName: "trash")
@@ -34,7 +34,7 @@ struct AnalyticsSettingsScreenStrings {
let sectionFooter: AttributedString
init(termsURL: URL) {
let content = AttributedString(L10n.screenAnalyticsSettingsHelpUsImprove(InfoPlistReader.main.bundleDisplayName))
let content = AttributedString(L10n.screenAnalyticsPromptHelpUsImprove)
// Create the 'read terms' with a placeholder.
let linkPlaceholder = "{link}"
+47 -30
View File
@@ -46,6 +46,11 @@ class RoomProxy: RoomProxyProtocol {
updatesSubject.eraseToAnyPublisher()
}
var innerTimelineProvider: RoomTimelineProviderProtocol!
var timelineProvider: RoomTimelineProviderProtocol {
innerTimelineProvider
}
deinit {
Task { @MainActor [roomTimelineObservationToken, roomListItem] in
roomTimelineObservationToken?.cancel()
@@ -59,6 +64,30 @@ class RoomProxy: RoomProxyProtocol {
self.roomListItem = roomListItem
self.room = room
self.backgroundTaskService = backgroundTaskService
let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""),
RequiredState(key: "m.room.topic", value: ""),
RequiredState(key: "m.room.avatar", value: ""),
RequiredState(key: "m.room.canonical_alias", value: ""),
RequiredState(key: "m.room.join_rules", value: "")],
timelineLimit: UInt32(SlidingSyncConstants.defaultTimelineLimit))
roomListItem.subscribe(settings: settings)
let timelineListener = RoomTimelineListener { [weak self] timelineDiff in
self?.updatesSubject.send(timelineDiff)
}
self.timelineListener = timelineListener
let result = room.addTimelineListener(listener: timelineListener)
roomTimelineObservationToken = result.itemsStream
innerTimelineProvider = RoomTimelineProvider(currentItems: result.items, updatePublisher: updatesPublisher)
Task {
await fetchMembers()
await updateMembers()
}
}
lazy var id: String = room.id()
@@ -162,36 +191,6 @@ class RoomProxy: RoomProxyProtocol {
}
}
func registerTimelineListenerIfNeeded() -> Result<[TimelineItem], RoomProxyError> {
guard timelineListener == nil else {
return .failure(.roomListenerAlreadyRegistered)
}
let settings = RoomSubscription(requiredState: [RequiredState(key: "m.room.name", value: ""),
RequiredState(key: "m.room.topic", value: ""),
RequiredState(key: "m.room.avatar", value: ""),
RequiredState(key: "m.room.canonical_alias", value: ""),
RequiredState(key: "m.room.join_rules", value: "")],
timelineLimit: UInt32(SlidingSyncConstants.defaultTimelineLimit))
roomListItem.subscribe(settings: settings)
let timelineListener = RoomTimelineListener { [weak self] timelineDiff in
self?.updatesSubject.send(timelineDiff)
}
self.timelineListener = timelineListener
let result = room.addTimelineListener(listener: timelineListener)
roomTimelineObservationToken = result.itemsStream
Task {
await fetchMembers()
await updateMembers()
}
return .success(result.items)
}
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError> {
do {
try await Task.dispatch(on: .global()) {
@@ -219,6 +218,24 @@ class RoomProxy: RoomProxyProtocol {
}
}
}
func messageEventContent(for eventID: String) -> RoomMessageEventContent? {
try? room.getTimelineEventContentByEventId(eventId: eventID)
}
func sendMessageEventContent(_ messageContent: RoomMessageEventContent) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
defer {
sendMessageBackgroundTask?.stop()
}
let transactionId = genTransactionId()
return await Task.dispatch(on: userInitiatedDispatchQueue) {
self.room.send(msg: messageContent, txnId: transactionId)
return .success(())
}
}
func sendMessage(_ message: String, inReplyTo eventID: String? = nil) async -> Result<Void, RoomProxyError> {
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
@@ -20,7 +20,6 @@ import MatrixRustSDK
enum RoomProxyError: Error {
case noMoreMessagesToBackPaginate
case roomListenerAlreadyRegistered
case failedPaginatingBackwards
case failedRetrievingMemberAvatarURL
case failedRetrievingMemberDisplayName
@@ -73,22 +72,23 @@ protocol RoomProxyProtocol {
var activeMembersCount: Int { get }
/// Publishes the room's updates.
/// The publisher starts publishing after the first call to `registerTimelineListenerIfNeeded()`
/// The thread on which this publisher sends the output isn't defined.
var updatesPublisher: AnyPublisher<TimelineDiff, Never> { get }
var timelineProvider: RoomTimelineProviderProtocol { get }
func loadAvatarURLForUserId(_ userId: String) async -> Result<URL?, RoomProxyError>
func loadDisplayNameForUserId(_ userId: String) async -> Result<String?, RoomProxyError>
/// Registers a timeline listener if not registered already.
/// Updates for this object will be published on the `updatesPublisher` publisher.
func registerTimelineListenerIfNeeded() -> Result<[TimelineItem], RoomProxyError>
func paginateBackwards(requestSize: UInt, untilNumberOfItems: UInt) async -> Result<Void, RoomProxyError>
func sendReadReceipt(for eventID: String) async -> Result<Void, RoomProxyError>
func messageEventContent(for eventID: String) -> RoomMessageEventContent?
func sendMessageEventContent(_ messageContent: RoomMessageEventContent) async -> Result<Void, RoomProxyError>
func sendMessage(_ message: String, inReplyTo eventID: String?) async -> Result<Void, RoomProxyError>
func sendReaction(_ reaction: String, to eventID: String) async -> Result<Void, RoomProxyError>
@@ -19,7 +19,6 @@ import Foundation
import MatrixRustSDK
class RoomTimelineProvider: RoomTimelineProviderProtocol {
private let roomProxy: RoomProxyProtocol
private var cancellables = Set<AnyCancellable>()
private let serialDispatchQueue: DispatchQueue
@@ -35,27 +34,15 @@ class RoomTimelineProvider: RoomTimelineProviderProtocol {
}
}
init(roomProxy: RoomProxyProtocol) {
self.roomProxy = roomProxy
init(currentItems: [TimelineItem], updatePublisher: AnyPublisher<TimelineDiff, Never>) {
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
itemProxies = []
itemProxies = currentItems.map(TimelineItemProxy.init)
roomProxy
.updatesPublisher
updatePublisher
.collect(.byTime(serialDispatchQueue, 0.1))
.sink { [weak self] in self?.updateItemsWithDiffs($0) }
.store(in: &cancellables)
switch roomProxy.registerTimelineListenerIfNeeded() {
case let .success(items):
itemProxies = items.map(TimelineItemProxy.init)
MXLog.verbose("Added timeline listener, current items (\(items.count)) : \(items.map(\.debugIdentifier))")
case .failure(.roomListenerAlreadyRegistered):
MXLog.verbose("Listener already registered for the room: \(roomProxy.id)")
case .failure:
let roomID = roomProxy.id
MXLog.error("Failed adding timeline listener on room with identifier: \(roomID)")
}
}
// MARK: - Private
@@ -68,7 +68,7 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol {
func debugInfo(for itemID: String) -> TimelineItemDebugInfo {
.init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil)
}
func retryDecryption(for sessionID: String) async { }
// MARK: - UI Test signalling
@@ -43,17 +43,16 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
init(userId: String,
roomProxy: RoomProxyProtocol,
timelineProvider: RoomTimelineProviderProtocol,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
mediaProvider: MediaProviderProtocol) {
self.userId = userId
self.timelineProvider = timelineProvider
self.roomProxy = roomProxy
timelineProvider = roomProxy.timelineProvider
self.timelineItemFactory = timelineItemFactory
self.mediaProvider = mediaProvider
self.roomProxy = roomProxy
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
self.timelineProvider
timelineProvider
.itemsPublisher
.receive(on: serialDispatchQueue)
.sink { [weak self] _ in
@@ -23,7 +23,6 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
mediaProvider: MediaProviderProtocol) -> RoomTimelineControllerProtocol {
RoomTimelineController(userId: userId,
roomProxy: roomProxy,
timelineProvider: RoomTimelineProvider(roomProxy: roomProxy),
timelineItemFactory: timelineItemFactory,
mediaProvider: mediaProvider)
}
@@ -0,0 +1,21 @@
//
// 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 MessageForwardingScreenUITests: XCTestCase { }
@@ -0,0 +1,69 @@
//
// 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 XCTest
@testable import ElementX
@MainActor
class MessageForwardingScreenViewModelTests: XCTestCase {
var viewModel: MessageForwardingScreenViewModelProtocol!
var context: MessageForwardingScreenViewModelType.Context!
var cancellables = Set<AnyCancellable>()
override func setUpWithError() throws {
viewModel = MessageForwardingScreenViewModel(roomSummaryProvider: MockRoomSummaryProvider(state: .loaded(.mockRooms)), sourceRoomID: "1")
context = viewModel.context
}
func testInitialState() {
XCTAssertNil(context.viewState.rooms.first(where: { $0.id == "1" }), "The source room ID shouldn't be shown")
}
func testRoomSelection() {
context.send(viewAction: .selectRoom(roomID: "2"))
XCTAssertEqual(context.viewState.selectedRoomID, "2")
}
func testSearching() {
context.searchQuery = "Second"
XCTAssertEqual(context.viewState.visibleRooms.count, 1)
}
func testForwarding() {
context.send(viewAction: .selectRoom(roomID: "2"))
XCTAssertEqual(context.viewState.selectedRoomID, "2")
let expectation = expectation(description: "Wait for confirmation")
viewModel.actions
.sink { action in
switch action {
case .send(let roomID):
XCTAssertEqual(roomID, "2")
expectation.fulfill()
default:
break
}
}
.store(in: &cancellables)
context.send(viewAction: .send)
waitForExpectations(timeout: 5.0)
}
}
@@ -221,11 +221,7 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
await Task.yield()
XCTAssertTrue(callbackCorrectlyCalled)
}
func testRoomSubscription() async {
XCTAssertEqual(roomProxyMock.registerTimelineListenerIfNeededCallsCount, 1)
}
func testCanEditAvatar() async {
let owner: RoomMemberProxyMock = .mockOwner(allowedStateEvents: [.roomAvatar])
let mockedMembers: [RoomMemberProxyMock] = [owner, .mockBob, .mockAlice]
@@ -38,7 +38,8 @@ class RoomFlowCoordinatorTests: XCTestCase {
roomTimelineControllerFactory: MockRoomTimelineControllerFactory(),
navigationStackCoordinator: navigationStackCoordinator,
navigationSplitCoordinator: navigationSplitCoordinator,
emojiProvider: EmojiProvider())
emojiProvider: EmojiProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController)
}
func testRoomPresentation() async {
+1
View File
@@ -0,0 +1 @@
Added support for message forwarding