mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Introduce a TimelineItemThreadSummary object (#4032)
* Introduce a `TimelineItemThreadSummary` object to hold details about threads starting from that particular item This patch introduces a thread summary object that will be available on main timeline messages that are the root for a given thread. It currently provides the latest message content and sender for that thread but it will grow to provide info on the number of replies, unreads etc. It also add a new UI component called `TimelineThreadSummaryView` that makes use of this data and is in turn used by the bubbled styler to render it in the timeline. The rest of the PR is about refactoring on the `RoomTimelineItemFactory` so that replies and thread summaries use similar paths and builders. * Add a feature flag for threads * Address PR comments * Converge on single implementation for message previews
This commit is contained in:
@@ -141,6 +141,7 @@
|
|||||||
17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; };
|
17BC15DA08A52587466698C5 /* RoomMessageEventStringBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E815FF3CC5E5A355E3A25E /* RoomMessageEventStringBuilder.swift */; };
|
||||||
1801F1467ABCEA080419E150 /* preview_avatar_user.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 87FC42213E86E8182CFD3A49 /* preview_avatar_user.jpg */; };
|
1801F1467ABCEA080419E150 /* preview_avatar_user.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 87FC42213E86E8182CFD3A49 /* preview_avatar_user.jpg */; };
|
||||||
182D532B736178A1DED9F76E /* ReportRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FCAE847556719BBE7A0882 /* ReportRoomScreenModels.swift */; };
|
182D532B736178A1DED9F76E /* ReportRoomScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11FCAE847556719BBE7A0882 /* ReportRoomScreenModels.swift */; };
|
||||||
|
18386B777FDA74E4B3282D4F /* TimelineItemThreadSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C6A082F2B2A15E1B9BE280 /* TimelineItemThreadSummary.swift */; };
|
||||||
18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; };
|
18867F4F1C8991EEC56EA932 /* UTType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 897DF5E9A70CE05A632FC8AF /* UTType.swift */; };
|
||||||
18978C9438206828C1D5AF2A /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */; };
|
18978C9438206828C1D5AF2A /* test_animated_image.gif in Resources */ = {isa = PBXBuildFile; fileRef = 53FD6D3D38F556CEAA280C58 /* test_animated_image.gif */; };
|
||||||
18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; };
|
18E3786918486D4C9726BC84 /* FormButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FBFC09F9DAFF1E4BA97849 /* FormButtonStyles.swift */; };
|
||||||
@@ -187,6 +188,7 @@
|
|||||||
21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */; };
|
21F29351EDD7B2A5534EE862 /* SecureBackupKeyBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD558A898847C179E4B7A237 /* SecureBackupKeyBackupScreen.swift */; };
|
||||||
22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
|
22882C710BC99EC34A5024A0 /* UITestsScreenIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CEBE5EA91E8691EDF364EC2 /* UITestsScreenIdentifier.swift */; };
|
||||||
22B380C579C148BA0BFB5952 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
|
22B380C579C148BA0BFB5952 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3557ACB95D0F666EF5AF0CE /* Secrets.swift */; };
|
||||||
|
22BA593A5E19D8D3DE2FAA6F /* TimelineEventContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */; };
|
||||||
22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */; };
|
22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */; };
|
||||||
2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */; };
|
2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */; };
|
||||||
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; };
|
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; };
|
||||||
@@ -655,6 +657,7 @@
|
|||||||
7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */; };
|
7F7EA51A9A43125A8CB6AC90 /* NotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46D560DDA3B20C82766ACFAD /* NotificationSettingsScreenViewModel.swift */; };
|
||||||
7F825CBD857D65DC986087BA /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */; };
|
7F825CBD857D65DC986087BA /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F54FA7C5CB7B342EF9B9B2F /* NoticeRoomTimelineView.swift */; };
|
||||||
7F941B063C94E1718DFC2CF3 /* RoomChangeRolesScreenRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E6EB7960BC9D0F7396B3BD /* RoomChangeRolesScreenRow.swift */; };
|
7F941B063C94E1718DFC2CF3 /* RoomChangeRolesScreenRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23E6EB7960BC9D0F7396B3BD /* RoomChangeRolesScreenRow.swift */; };
|
||||||
|
7F99B6DDBEC5D5E6752C7276 /* TimelineThreadSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF2E6ADAE685F4109B1FE795 /* TimelineThreadSummaryView.swift */; };
|
||||||
7FED77802940EA7DF4D0D3A2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */; };
|
7FED77802940EA7DF4D0D3A2 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 36DA824791172B9821EACBED /* PrivacyInfo.xcprivacy */; };
|
||||||
7FF27DA70D833CFC5724EFC5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C07EA60CAB296D7726210F5B /* MatrixRustSDK */; };
|
7FF27DA70D833CFC5724EFC5 /* MatrixRustSDK in Frameworks */ = {isa = PBXBuildFile; productRef = C07EA60CAB296D7726210F5B /* MatrixRustSDK */; };
|
||||||
7FF6E1FBE6E9517FD29A1D8E /* RoomChangeRolesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */; };
|
7FF6E1FBE6E9517FD29A1D8E /* RoomChangeRolesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A5C34C4E4268EF65D171EF /* RoomChangeRolesScreenModels.swift */; };
|
||||||
@@ -1606,6 +1609,7 @@
|
|||||||
2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = "<group>"; };
|
2EFE1922F39398ABFB36DF3F /* RoomDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = "<group>"; };
|
2F06F70B9C433BAD4BC6B9F5 /* EncryptedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||||
2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = "<group>"; };
|
2F36C5D9B37E50915ECBD3EE /* RoomMemberProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberProxy.swift; sourceTree = "<group>"; };
|
||||||
|
2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineEventContent.swift; sourceTree = "<group>"; };
|
||||||
303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModel.swift; sourceTree = "<group>"; };
|
303D9438EFB481F57A366E82 /* EncryptionResetPasswordScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetPasswordScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModel.swift; sourceTree = "<group>"; };
|
303FCADE77DF1F3670C086ED /* BugReportScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenModels.swift; sourceTree = "<group>"; };
|
306AB507E1027D6C5C147EB6 /* EncryptionResetScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionResetScreenModels.swift; sourceTree = "<group>"; };
|
||||||
@@ -2099,6 +2103,7 @@
|
|||||||
981663D961C94270FA035FD0 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
|
981663D961C94270FA035FD0 /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; };
|
||||||
989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelTests.swift; sourceTree = "<group>"; };
|
989D7380D9C86B3A10D30B13 /* AppLockSetupPINScreenViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockSetupPINScreenViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
989FC684408B31A677F5538B /* CompletionSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionView.swift; sourceTree = "<group>"; };
|
989FC684408B31A677F5538B /* CompletionSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionView.swift; sourceTree = "<group>"; };
|
||||||
|
98C6A082F2B2A15E1B9BE280 /* TimelineItemThreadSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemThreadSummary.swift; sourceTree = "<group>"; };
|
||||||
997BF045585AF6DB2EBC5755 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
997BF045585AF6DB2EBC5755 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = "<group>"; };
|
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = "<group>"; };
|
9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = "<group>"; };
|
||||||
@@ -2206,6 +2211,7 @@
|
|||||||
AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFNumberedListView.swift; sourceTree = "<group>"; };
|
AEB5FF7A09B79B0C6B528F7C /* SFNumberedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFNumberedListView.swift; sourceTree = "<group>"; };
|
||||||
AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = "<group>"; };
|
AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = "<group>"; };
|
||||||
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = "<group>"; };
|
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = "<group>"; };
|
||||||
|
AF2E6ADAE685F4109B1FE795 /* TimelineThreadSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineThreadSummaryView.swift; sourceTree = "<group>"; };
|
||||||
AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = "<group>"; };
|
AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = "<group>"; };
|
||||||
AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = "<group>"; };
|
AF8548D48512127CCC17C520 /* PollRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||||
AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = "<group>"; };
|
AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||||
@@ -3075,6 +3081,14 @@
|
|||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
27256E3989A52CF8EEB8AF5F /* Threads */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AF2E6ADAE685F4109B1FE795 /* TimelineThreadSummaryView.swift */,
|
||||||
|
);
|
||||||
|
path = Threads;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
2774D635E78D8B98390EA694 /* Resources */ = {
|
2774D635E78D8B98390EA694 /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -4141,7 +4155,9 @@
|
|||||||
955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */,
|
955336CBD5ED73C792D1F580 /* EncryptionAuthenticity.swift */,
|
||||||
314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */,
|
314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */,
|
||||||
5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */,
|
5DE8D25D6A91030175D52A20 /* RoomTimelineItemProperties.swift */,
|
||||||
|
2F926D08EB3D622A480BCA71 /* TimelineEventContent.swift */,
|
||||||
BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */,
|
BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */,
|
||||||
|
98C6A082F2B2A15E1B9BE280 /* TimelineItemThreadSummary.swift */,
|
||||||
);
|
);
|
||||||
path = TimelineItemContent;
|
path = TimelineItemContent;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -6077,6 +6093,7 @@
|
|||||||
7F6468993CB943953DC355A6 /* Replies */,
|
7F6468993CB943953DC355A6 /* Replies */,
|
||||||
308FE2283B9803DBBB05602C /* Style */,
|
308FE2283B9803DBBB05602C /* Style */,
|
||||||
13CC0A77B6E5EEDF881C5E8B /* Supplementary */,
|
13CC0A77B6E5EEDF881C5E8B /* Supplementary */,
|
||||||
|
27256E3989A52CF8EEB8AF5F /* Threads */,
|
||||||
CC2B160BAFD5FEAC106F89F3 /* TimelineItemViews */,
|
CC2B160BAFD5FEAC106F89F3 /* TimelineItemViews */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
@@ -7674,6 +7691,7 @@
|
|||||||
B47213F07A67CE3A8D49CEC9 /* TimelineControllerFactoryProtocol.swift in Sources */,
|
B47213F07A67CE3A8D49CEC9 /* TimelineControllerFactoryProtocol.swift in Sources */,
|
||||||
FA53FA227FFBE469AFF32F71 /* TimelineControllerProtocol.swift in Sources */,
|
FA53FA227FFBE469AFF32F71 /* TimelineControllerProtocol.swift in Sources */,
|
||||||
798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */,
|
798BF3072137833FBD3F4C96 /* TimelineDeliveryStatusView.swift in Sources */,
|
||||||
|
22BA593A5E19D8D3DE2FAA6F /* TimelineEventContent.swift in Sources */,
|
||||||
109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */,
|
109AEB7D33C4497727AFB87F /* TimelineInteractionHandler.swift in Sources */,
|
||||||
E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */,
|
E6FA87F773424B27614B23E9 /* TimelineItemAccessibilityModifier.swift in Sources */,
|
||||||
F6767F17D538578B370DD805 /* TimelineItemBubbleBackground.swift in Sources */,
|
F6767F17D538578B370DD805 /* TimelineItemBubbleBackground.swift in Sources */,
|
||||||
@@ -7689,6 +7707,7 @@
|
|||||||
F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */,
|
F3ECA377FF77E81A4F1FA062 /* TimelineItemSendInfoLabel.swift in Sources */,
|
||||||
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */,
|
1B88BB631F7FC45A213BB554 /* TimelineItemSender.swift in Sources */,
|
||||||
EFBBD44C0A16F017C32D2099 /* TimelineItemStatusView.swift in Sources */,
|
EFBBD44C0A16F017C32D2099 /* TimelineItemStatusView.swift in Sources */,
|
||||||
|
18386B777FDA74E4B3282D4F /* TimelineItemThreadSummary.swift in Sources */,
|
||||||
562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */,
|
562EFB9AB62B38830D9AA778 /* TimelineMediaFrame.swift in Sources */,
|
||||||
3684AD01C5FCB7616B28F629 /* TimelineMediaPreviewController.swift in Sources */,
|
3684AD01C5FCB7616B28F629 /* TimelineMediaPreviewController.swift in Sources */,
|
||||||
2A56B00B070F83E0FE571193 /* TimelineMediaPreviewDataSource.swift in Sources */,
|
2A56B00B070F83E0FE571193 /* TimelineMediaPreviewDataSource.swift in Sources */,
|
||||||
@@ -7714,6 +7733,7 @@
|
|||||||
F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */,
|
F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */,
|
||||||
7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */,
|
7A0D335D38ECA095A575B4F7 /* TimelineStyler.swift in Sources */,
|
||||||
6583A95947E78767736CB51A /* TimelineTableViewController.swift in Sources */,
|
6583A95947E78767736CB51A /* TimelineTableViewController.swift in Sources */,
|
||||||
|
7F99B6DDBEC5D5E6752C7276 /* TimelineThreadSummaryView.swift in Sources */,
|
||||||
67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */,
|
67EFF46180B939CBF389AECD /* TimelineView.swift in Sources */,
|
||||||
98EE4259A4A49BC757BA442C /* TimelineViewModel.swift in Sources */,
|
98EE4259A4A49BC757BA442C /* TimelineViewModel.swift in Sources */,
|
||||||
F8B2F5CBCF2A0E0798E8D646 /* TimelineViewModelProtocol.swift in Sources */,
|
F8B2F5CBCF2A0E0798E8D646 /* TimelineViewModelProtocol.swift in Sources */,
|
||||||
@@ -8635,7 +8655,7 @@
|
|||||||
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
repositoryURL = "https://github.com/element-hq/matrix-rust-components-swift";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = exactVersion;
|
kind = exactVersion;
|
||||||
version = 25.04.14;
|
version = 25.04.16;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */ = {
|
||||||
|
|||||||
@@ -158,8 +158,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
"location" : "https://github.com/element-hq/matrix-rust-components-swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "1f43ea9360f43ecd3d6674048265823709c96f1a",
|
"revision" : "895ba54c171859cace965e77b6fe2f64919ecb27",
|
||||||
"version" : "25.4.14"
|
"version" : "25.4.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ final class AppSettings {
|
|||||||
case knockingEnabled
|
case knockingEnabled
|
||||||
case reportRoomEnabled
|
case reportRoomEnabled
|
||||||
case reportInviteEnabled
|
case reportInviteEnabled
|
||||||
|
case threadsEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||||
@@ -338,6 +339,9 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.reportInviteEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.reportInviteEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
var reportInviteEnabled
|
var reportInviteEnabled
|
||||||
|
|
||||||
|
@UserPreference(key: UserDefaultsKeys.threadsEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
|
var threadsEnabled
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// MARK: - Shared
|
// MARK: - Shared
|
||||||
|
|||||||
@@ -20353,6 +20353,82 @@ open class TaskHandleSDKMock: MatrixRustSDK.TaskHandle, @unchecked Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
open class ThreadSummarySDKMock: MatrixRustSDK.ThreadSummary, @unchecked Sendable {
|
||||||
|
init() {
|
||||||
|
super.init(noPointer: .init())
|
||||||
|
}
|
||||||
|
|
||||||
|
public required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||||
|
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate var pointer: UnsafeMutableRawPointer!
|
||||||
|
|
||||||
|
//MARK: - latestEvent
|
||||||
|
|
||||||
|
var latestEventUnderlyingCallsCount = 0
|
||||||
|
open var latestEventCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return latestEventUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = latestEventUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
latestEventUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
latestEventUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var latestEventCalled: Bool {
|
||||||
|
return latestEventCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestEventUnderlyingReturnValue: ThreadSummaryLatestEventDetails!
|
||||||
|
open var latestEventReturnValue: ThreadSummaryLatestEventDetails! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return latestEventUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: ThreadSummaryLatestEventDetails? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = latestEventUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
latestEventUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
latestEventUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var latestEventClosure: (() -> ThreadSummaryLatestEventDetails)?
|
||||||
|
|
||||||
|
open override func latestEvent() -> ThreadSummaryLatestEventDetails {
|
||||||
|
latestEventCallsCount += 1
|
||||||
|
if let latestEventClosure = latestEventClosure {
|
||||||
|
return latestEventClosure()
|
||||||
|
} else {
|
||||||
|
return latestEventReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
open class TimelineSDKMock: MatrixRustSDK.Timeline, @unchecked Sendable {
|
open class TimelineSDKMock: MatrixRustSDK.Timeline, @unchecked Sendable {
|
||||||
init() {
|
init() {
|
||||||
super.init(noPointer: .init())
|
super.init(noPointer: .init())
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ struct EventTimelineItemSDKMockConfiguration {
|
|||||||
var sender = ""
|
var sender = ""
|
||||||
var senderProfile: ProfileDetails?
|
var senderProfile: ProfileDetails?
|
||||||
var isOwn = false
|
var isOwn = false
|
||||||
var content: TimelineItemContent = .msgLike(content: .init(kind: .redacted, reactions: [], threadRoot: nil, inReplyTo: nil))
|
var content: TimelineItemContent = .msgLike(content: .init(kind: .redacted,
|
||||||
|
reactions: [],
|
||||||
|
inReplyTo: nil,
|
||||||
|
threadRoot: nil,
|
||||||
|
threadSummary: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
extension EventTimelineItem {
|
extension EventTimelineItem {
|
||||||
@@ -44,8 +48,9 @@ extension EventTimelineItem {
|
|||||||
isEdited: false,
|
isEdited: false,
|
||||||
mentions: nil)),
|
mentions: nil)),
|
||||||
reactions: [],
|
reactions: [],
|
||||||
|
inReplyTo: nil,
|
||||||
threadRoot: nil,
|
threadRoot: nil,
|
||||||
inReplyTo: nil))
|
threadSummary: nil))
|
||||||
|
|
||||||
return .init(configuration: .init(content: content))
|
return .init(configuration: .init(content: content))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ enum UserAvatarSizeOnScreen {
|
|||||||
case mediaPreviewDetails
|
case mediaPreviewDetails
|
||||||
case sendInviteConfirmation
|
case sendInviteConfirmation
|
||||||
case sessionVerification
|
case sessionVerification
|
||||||
|
case threadSummary
|
||||||
|
|
||||||
var value: CGFloat {
|
var value: CGFloat {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -119,6 +120,8 @@ enum UserAvatarSizeOnScreen {
|
|||||||
return 64
|
return 64
|
||||||
case .sessionVerification:
|
case .sessionVerification:
|
||||||
return 52
|
return 52
|
||||||
|
case .threadSummary:
|
||||||
|
return 28
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,6 +313,7 @@ struct ComposerToolbar: View {
|
|||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
||||||
|
static let viewModel = TimelineViewModel.mock
|
||||||
static let wysiwygViewModel = WysiwygComposerViewModel()
|
static let wysiwygViewModel = WysiwygComposerViewModel()
|
||||||
static let composerViewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
|
static let composerViewModel = ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()),
|
||||||
wysiwygViewModel: wysiwygViewModel,
|
wysiwygViewModel: wysiwygViewModel,
|
||||||
@@ -352,6 +353,7 @@ struct ComposerToolbar_Previews: PreviewProvider, TestablePreview {
|
|||||||
ComposerToolbar.replyLoadingPreviewMock(isLoading: true)
|
ComposerToolbar.replyLoadingPreviewMock(isLoading: true)
|
||||||
ComposerToolbar.replyLoadingPreviewMock(isLoading: false)
|
ComposerToolbar.replyLoadingPreviewMock(isLoading: false)
|
||||||
}
|
}
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
.previewDisplayName("Reply")
|
.previewDisplayName("Reply")
|
||||||
|
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ struct MessageComposer_Previews: PreviewProvider, TestablePreview {
|
|||||||
mode: .edit(originalEventOrTransactionID: .eventID(UUID().uuidString), type: .editCaption))
|
mode: .edit(originalEventOrTransactionID: .eventID(UUID().uuidString), type: .editCaption))
|
||||||
}
|
}
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 8) {
|
VStack(spacing: 8) {
|
||||||
|
|||||||
+1
@@ -46,6 +46,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
|
|||||||
var knockingEnabled: Bool { get set }
|
var knockingEnabled: Bool { get set }
|
||||||
var reportRoomEnabled: Bool { get set }
|
var reportRoomEnabled: Bool { get set }
|
||||||
var reportInviteEnabled: Bool { get set }
|
var reportInviteEnabled: Bool { get set }
|
||||||
|
var threadsEnabled: Bool { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppSettings: DeveloperOptionsProtocol { }
|
extension AppSettings: DeveloperOptionsProtocol { }
|
||||||
|
|||||||
+6
@@ -31,6 +31,12 @@ struct DeveloperOptionsScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Section("General") {
|
||||||
|
Toggle(isOn: $context.threadsEnabled) {
|
||||||
|
Text("Threads")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Section("Room List") {
|
Section("Room List") {
|
||||||
Toggle(isOn: $context.publicSearchEnabled) {
|
Toggle(isOn: $context.publicSearchEnabled) {
|
||||||
Text("Public search")
|
Text("Public search")
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ enum TimelineViewAction {
|
|||||||
case displayReactionSummary(itemID: TimelineItemIdentifier, key: String)
|
case displayReactionSummary(itemID: TimelineItemIdentifier, key: String)
|
||||||
case displayEmojiPicker(itemID: TimelineItemIdentifier)
|
case displayEmojiPicker(itemID: TimelineItemIdentifier)
|
||||||
case displayReadReceipts(itemID: TimelineItemIdentifier)
|
case displayReadReceipts(itemID: TimelineItemIdentifier)
|
||||||
|
case displayThread(itemID: TimelineItemIdentifier)
|
||||||
|
|
||||||
case handlePasteOrDrop(provider: NSItemProvider)
|
case handlePasteOrDrop(provider: NSItemProvider)
|
||||||
case handlePollAction(TimelineViewPollAction)
|
case handlePollAction(TimelineViewPollAction)
|
||||||
@@ -102,6 +103,7 @@ struct TimelineViewState: BindableState {
|
|||||||
var canCurrentUserKick = false
|
var canCurrentUserKick = false
|
||||||
var canCurrentUserBan = false
|
var canCurrentUserBan = false
|
||||||
var isViewSourceEnabled: Bool
|
var isViewSourceEnabled: Bool
|
||||||
|
var areThreadsEnabled: Bool
|
||||||
var hideTimelineMedia: Bool
|
var hideTimelineMedia: Bool
|
||||||
|
|
||||||
// The `pinnedEventIDs` are used only to determine if an item is already pinned or not.
|
// The `pinnedEventIDs` are used only to determine if an item is already pinned or not.
|
||||||
@@ -238,3 +240,56 @@ enum ScrollDirection: Equatable {
|
|||||||
case top
|
case top
|
||||||
case bottom
|
case bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension TimelineViewState {
|
||||||
|
/// The string shown as the message preview.
|
||||||
|
///
|
||||||
|
/// This converts the formatted body to a plain string to remove formatting
|
||||||
|
/// and render with a consistent font size. This conversion is done to avoid
|
||||||
|
/// showing markdown characters in the preview for messages with formatting.
|
||||||
|
func buildMessagePreview(formattedBody: AttributedString?, plainBody: String) -> String {
|
||||||
|
guard let formattedBody,
|
||||||
|
let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else {
|
||||||
|
return plainBody
|
||||||
|
}
|
||||||
|
|
||||||
|
let range = NSRange(location: 0, length: attributedString.length)
|
||||||
|
attributedString.enumerateAttributes(in: range) { attributes, range, _ in
|
||||||
|
if let userID = attributes[.MatrixUserID] as? String {
|
||||||
|
if let displayName = members[userID]?.displayName {
|
||||||
|
attributedString.replaceCharacters(in: range, with: "@\(displayName)")
|
||||||
|
} else {
|
||||||
|
attributedString.replaceCharacters(in: range, with: userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attributes[.MatrixAllUsersMention] as? Bool == true {
|
||||||
|
attributedString.replaceCharacters(in: range, with: PillUtilities.atRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let roomAlias = attributes[.MatrixRoomAlias] as? String {
|
||||||
|
let roomName = roomNameForAliasResolver?(roomAlias)
|
||||||
|
attributedString.replaceCharacters(in: range, with: PillUtilities.roomPillDisplayText(roomName: roomName, rawRoomText: roomAlias))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let roomID = attributes[.MatrixRoomID] as? String {
|
||||||
|
let roomName = roomNameForIDResolver?(roomID)
|
||||||
|
attributedString.replaceCharacters(in: range, with: PillUtilities.roomPillDisplayText(roomName: roomName, rawRoomText: roomID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let eventOnRoomID = attributes[.MatrixEventOnRoomID] as? EventOnRoomIDAttribute.Value {
|
||||||
|
let roomID = eventOnRoomID.roomID
|
||||||
|
let roomName = roomNameForIDResolver?(roomID)
|
||||||
|
attributedString.replaceCharacters(in: range, with: PillUtilities.eventPillDisplayText(roomName: roomName, rawRoomText: roomID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let eventOnRoomAlias = attributes[.MatrixEventOnRoomAlias] as? EventOnRoomAliasAttribute.Value {
|
||||||
|
let roomAlias = eventOnRoomAlias.alias
|
||||||
|
let roomName = roomNameForAliasResolver?(roomAlias)
|
||||||
|
attributedString.replaceCharacters(in: range, with: PillUtilities.eventPillDisplayText(roomName: roomName, rawRoomText: eventOnRoomAlias.alias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributedString.string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
|
timelineState: TimelineState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }),
|
||||||
ownUserID: roomProxy.ownUserID,
|
ownUserID: roomProxy.ownUserID,
|
||||||
isViewSourceEnabled: appSettings.viewSourceEnabled,
|
isViewSourceEnabled: appSettings.viewSourceEnabled,
|
||||||
|
areThreadsEnabled: appSettings.threadsEnabled,
|
||||||
hideTimelineMedia: hideTimelineMedia,
|
hideTimelineMedia: hideTimelineMedia,
|
||||||
pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs,
|
pinnedEventIDs: roomProxy.infoPublisher.value.pinnedEventIDs,
|
||||||
emojiProvider: emojiProvider,
|
emojiProvider: emojiProvider,
|
||||||
@@ -182,14 +183,16 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
|||||||
timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID)
|
timelineInteractionHandler.displayTimelineItemActionMenu(for: itemID)
|
||||||
case .handleTimelineItemMenuAction(let itemID, let action):
|
case .handleTimelineItemMenuAction(let itemID, let action):
|
||||||
timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID)
|
timelineInteractionHandler.handleTimelineItemMenuAction(action, itemID: itemID)
|
||||||
case .tappedOnSenderDetails(userID: let userID):
|
case .tappedOnSenderDetails(let userID):
|
||||||
handleTappedOnSenderDetails(userID: userID)
|
handleTappedOnSenderDetails(userID: userID)
|
||||||
case .displayEmojiPicker(let itemID):
|
case .displayEmojiPicker(let itemID):
|
||||||
timelineInteractionHandler.displayEmojiPicker(for: itemID)
|
timelineInteractionHandler.displayEmojiPicker(for: itemID)
|
||||||
case .displayReactionSummary(let itemID, let key):
|
case .displayReactionSummary(let itemID, let key):
|
||||||
displayReactionSummary(for: itemID, selectedKey: key)
|
displayReactionSummary(for: itemID, selectedKey: key)
|
||||||
case .displayReadReceipts(itemID: let itemID):
|
case .displayReadReceipts(let itemID):
|
||||||
displayReadReceipts(for: itemID)
|
displayReadReceipts(for: itemID)
|
||||||
|
case .displayThread:
|
||||||
|
break
|
||||||
case .handlePasteOrDrop(let provider):
|
case .handlePasteOrDrop(let provider):
|
||||||
timelineInteractionHandler.handlePasteOrDrop(provider)
|
timelineInteractionHandler.handlePasteOrDrop(provider)
|
||||||
case .handlePollAction(let pollAction):
|
case .handlePollAction(let pollAction):
|
||||||
|
|||||||
@@ -131,12 +131,12 @@ struct TimelineReplyView: View {
|
|||||||
.cornerRadius(icon?.cornerRadii ?? 0.0, corners: .allCorners)
|
.cornerRadius(icon?.cornerRadii ?? 0.0, corners: .allCorners)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(sender.displayName ?? sender.id)
|
Text(sender.disambiguatedDisplayName ?? sender.id)
|
||||||
.font(.compound.bodySMSemibold)
|
.font(.compound.bodySMSemibold)
|
||||||
.foregroundColor(.compound.textPrimary)
|
.foregroundColor(.compound.textPrimary)
|
||||||
.accessibilityLabel(L10n.commonInReplyTo(sender.displayName ?? sender.id))
|
.accessibilityLabel(L10n.commonInReplyTo(sender.disambiguatedDisplayName ?? sender.id))
|
||||||
|
|
||||||
Text(messagePreview)
|
Text(context.viewState.buildMessagePreview(formattedBody: formattedBody, plainBody: plainBody))
|
||||||
.font(.compound.bodyMD)
|
.font(.compound.bodyMD)
|
||||||
.foregroundColor(.compound.textSecondary)
|
.foregroundColor(.compound.textSecondary)
|
||||||
.tint(.compound.textLinkExternal)
|
.tint(.compound.textLinkExternal)
|
||||||
@@ -175,56 +175,6 @@ struct TimelineReplyView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The string shown as the message preview.
|
|
||||||
///
|
|
||||||
/// This converts the formatted body to a plain string to remove formatting
|
|
||||||
/// and render with a consistent font size. This conversion is done to avoid
|
|
||||||
/// showing markdown characters in the preview for messages with formatting.
|
|
||||||
private var messagePreview: String {
|
|
||||||
guard let formattedBody,
|
|
||||||
let attributedString = try? NSMutableAttributedString(formattedBody, including: \.elementX) else {
|
|
||||||
return plainBody
|
|
||||||
}
|
|
||||||
|
|
||||||
let range = NSRange(location: 0, length: attributedString.length)
|
|
||||||
attributedString.enumerateAttributes(in: range) { attributes, range, _ in
|
|
||||||
if let userID = attributes[.MatrixUserID] as? String {
|
|
||||||
if let displayName = context.viewState.members[userID]?.displayName {
|
|
||||||
attributedString.replaceCharacters(in: range, with: "@\(displayName)")
|
|
||||||
} else {
|
|
||||||
attributedString.replaceCharacters(in: range, with: userID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if attributes[.MatrixAllUsersMention] as? Bool == true {
|
|
||||||
attributedString.replaceCharacters(in: range, with: PillUtilities.atRoom)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let roomAlias = attributes[.MatrixRoomAlias] as? String {
|
|
||||||
let roomName = context.viewState.roomNameForAliasResolver?(roomAlias)
|
|
||||||
attributedString.replaceCharacters(in: range, with: PillUtilities.roomPillDisplayText(roomName: roomName, rawRoomText: roomAlias))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let roomID = attributes[.MatrixRoomID] as? String {
|
|
||||||
let roomName = context.viewState.roomNameForIDResolver?(roomID)
|
|
||||||
attributedString.replaceCharacters(in: range, with: PillUtilities.roomPillDisplayText(roomName: roomName, rawRoomText: roomID))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let eventOnRoomID = attributes[.MatrixEventOnRoomID] as? EventOnRoomIDAttribute.Value {
|
|
||||||
let roomID = eventOnRoomID.roomID
|
|
||||||
let roomName = context.viewState.roomNameForIDResolver?(roomID)
|
|
||||||
attributedString.replaceCharacters(in: range, with: PillUtilities.eventPillDisplayText(roomName: roomName, rawRoomText: roomID))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let eventOnRoomAlias = attributes[.MatrixEventOnRoomAlias] as? EventOnRoomAliasAttribute.Value {
|
|
||||||
let roomAlias = eventOnRoomAlias.alias
|
|
||||||
let roomName = context.viewState.roomNameForAliasResolver?(roomAlias)
|
|
||||||
attributedString.replaceCharacters(in: range, with: PillUtilities.eventPillDisplayText(roomName: roomName, rawRoomText: eventOnRoomAlias.alias))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attributedString.string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,13 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
// Workaround to stop the message long press stealing the touch from the reaction buttons
|
// Workaround to stop the message long press stealing the touch from the reaction buttons
|
||||||
.onTapGesture { }
|
.onTapGesture { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let threadSummary = timelineItem.properties.threadSummary {
|
||||||
|
TimelineThreadSummaryView(threadSummary: threadSummary) {
|
||||||
|
context.send(viewAction: .displayThread(itemID: timelineItem.id))
|
||||||
|
}
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,10 +347,14 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
replies
|
replies
|
||||||
.previewDisplayName("Replies")
|
.previewDisplayName("Replies")
|
||||||
threads
|
threadDecorator
|
||||||
.previewDisplayName("Thread decorator")
|
.previewDisplayName("Thread decorator")
|
||||||
.previewLayout(.fixed(width: 390, height: 1700))
|
.previewLayout(.fixed(width: 390, height: 1700))
|
||||||
.padding(.bottom, 20)
|
.padding(.bottom, 20)
|
||||||
|
threadSummary
|
||||||
|
.previewDisplayName("Thread summary")
|
||||||
|
.previewLayout(.fixed(width: 390, height: 1700))
|
||||||
|
.padding(.bottom, 20)
|
||||||
encryptionAuthenticity
|
encryptionAuthenticity
|
||||||
.previewDisplayName("Encryption Indicators")
|
.previewDisplayName("Encryption Indicators")
|
||||||
pinned
|
pinned
|
||||||
@@ -395,7 +406,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
.environment(\.timelineContext, viewModel.context)
|
.environment(\.timelineContext, viewModel.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
static var threads: some View {
|
static var threadDecorator: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
MockTimelineContent(isThreaded: true)
|
MockTimelineContent(isThreaded: true)
|
||||||
}
|
}
|
||||||
@@ -403,6 +414,18 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
.environment(\.timelineContext, viewModel.context)
|
.environment(\.timelineContext, viewModel.context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var threadSummary: some View {
|
||||||
|
ScrollView {
|
||||||
|
let threadSummary = TimelineItemThreadSummary.loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.text(.init(body: "This is a threaded message"))))
|
||||||
|
|
||||||
|
MockTimelineContent(threadSummary: threadSummary)
|
||||||
|
}
|
||||||
|
.environmentObject(viewModelWithPins.context)
|
||||||
|
.environment(\.timelineContext, viewModel.context)
|
||||||
|
}
|
||||||
|
|
||||||
static var pinned: some View {
|
static var pinned: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
MockTimelineContent(isPinned: true)
|
MockTimelineContent(isPinned: true)
|
||||||
@@ -493,6 +516,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
|||||||
private struct MockTimelineContent: View {
|
private struct MockTimelineContent: View {
|
||||||
var isThreaded = false
|
var isThreaded = false
|
||||||
var isPinned = false
|
var isPinned = false
|
||||||
|
var threadSummary: TimelineItemThreadSummary?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: makeItemIdentifier(),
|
RoomTimelineItemView(viewState: .init(item: TextRoomTimelineItem(id: makeItemIdentifier(),
|
||||||
@@ -502,7 +526,9 @@ private struct MockTimelineContent: View {
|
|||||||
canBeRepliedTo: true,
|
canBeRepliedTo: true,
|
||||||
sender: .init(id: "whoever"),
|
sender: .init(id: "whoever"),
|
||||||
content: .init(body: "A long message that should be on multiple lines."),
|
content: .init(body: "A long message that should be on multiple lines."),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)),
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)),
|
||||||
groupStyle: .single))
|
groupStyle: .single))
|
||||||
|
|
||||||
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||||
@@ -517,7 +543,9 @@ private struct MockTimelineContent: View {
|
|||||||
source: nil,
|
source: nil,
|
||||||
fileSize: nil,
|
fileSize: nil,
|
||||||
contentType: nil),
|
contentType: nil),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)))
|
||||||
|
|
||||||
FileRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
FileRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
@@ -531,7 +559,9 @@ private struct MockTimelineContent: View {
|
|||||||
fileSize: nil,
|
fileSize: nil,
|
||||||
thumbnailSource: nil,
|
thumbnailSource: nil,
|
||||||
contentType: nil),
|
contentType: nil),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)))
|
||||||
|
|
||||||
ImageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
ImageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
@@ -542,7 +572,9 @@ private struct MockTimelineContent: View {
|
|||||||
content: .init(filename: "image.jpg",
|
content: .init(filename: "image.jpg",
|
||||||
imageInfo: .mockImage,
|
imageInfo: .mockImage,
|
||||||
thumbnailInfo: nil),
|
thumbnailInfo: nil),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)))
|
||||||
|
|
||||||
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
@@ -554,7 +586,9 @@ private struct MockTimelineContent: View {
|
|||||||
geoURI: .init(latitude: 41.902782,
|
geoURI: .init(latitude: 41.902782,
|
||||||
longitude: 12.496366),
|
longitude: 12.496366),
|
||||||
description: "Location description description description description description description description description"),
|
description: "Location description description description description description description description description"),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)))
|
||||||
|
|
||||||
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
LocationRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
@@ -564,7 +598,9 @@ private struct MockTimelineContent: View {
|
|||||||
sender: .init(id: "Bob"),
|
sender: .init(id: "Bob"),
|
||||||
content: .init(body: "Fallback geo uri description",
|
content: .init(body: "Fallback geo uri description",
|
||||||
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
|
geoURI: .init(latitude: 41.902782, longitude: 12.496366), description: nil),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)))
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)))
|
||||||
|
|
||||||
VoiceMessageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
VoiceMessageRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||||
timestamp: .mock,
|
timestamp: .mock,
|
||||||
@@ -578,7 +614,9 @@ private struct MockTimelineContent: View {
|
|||||||
source: nil,
|
source: nil,
|
||||||
fileSize: nil,
|
fileSize: nil,
|
||||||
contentType: nil),
|
contentType: nil),
|
||||||
properties: .init(replyDetails: replyDetails, isThreaded: isThreaded)),
|
properties: .init(replyDetails: replyDetails,
|
||||||
|
isThreaded: isThreaded,
|
||||||
|
threadSummary: threadSummary)),
|
||||||
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
|
playerState: AudioPlayerState(id: .timelineItemIdentifier(.randomEvent),
|
||||||
title: L10n.commonVoiceMessage,
|
title: L10n.commonVoiceMessage,
|
||||||
duration: 10,
|
duration: 10,
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023, 2024 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Compound
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct TimelineThreadSummaryView: View {
|
||||||
|
let threadSummary: TimelineItemThreadSummary
|
||||||
|
var onTap: (() -> Void)?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
onTap?()
|
||||||
|
} label: {
|
||||||
|
content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var content: some View {
|
||||||
|
switch threadSummary {
|
||||||
|
case .loaded(let senderID, let sender, let latestEventContent):
|
||||||
|
switch latestEventContent {
|
||||||
|
case .message(let content):
|
||||||
|
switch content {
|
||||||
|
case .audio(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.caption ?? content.filename,
|
||||||
|
formattedBody: content.formattedCaption)
|
||||||
|
case .emote(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.body,
|
||||||
|
formattedBody: content.formattedBody)
|
||||||
|
case .file(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.caption ?? content.filename,
|
||||||
|
formattedBody: content.formattedCaption)
|
||||||
|
case .image(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.caption ?? content.filename,
|
||||||
|
formattedBody: content.formattedCaption)
|
||||||
|
case .notice(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.body,
|
||||||
|
formattedBody: content.formattedBody)
|
||||||
|
case .text(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.body,
|
||||||
|
formattedBody: content.formattedBody)
|
||||||
|
case .video(let content):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: content.caption ?? content.filename,
|
||||||
|
formattedBody: content.formattedCaption)
|
||||||
|
case .voice:
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: L10n.commonVoiceMessage,
|
||||||
|
formattedBody: nil)
|
||||||
|
case .location:
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: L10n.commonSharedLocation,
|
||||||
|
formattedBody: nil)
|
||||||
|
}
|
||||||
|
case .poll(let question):
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: question,
|
||||||
|
formattedBody: nil)
|
||||||
|
case .redacted:
|
||||||
|
ThreadView(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
plainBody: L10n.commonMessageRemoved,
|
||||||
|
formattedBody: nil)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
LoadingThreadView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct LoadingThreadView: View {
|
||||||
|
var body: some View {
|
||||||
|
ThreadView(senderID: "@alice:matrix.org", sender: nil, plainBody: "Hello world", formattedBody: nil)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ThreadView: View {
|
||||||
|
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||||
|
|
||||||
|
let senderID: String
|
||||||
|
let sender: TimelineItemSender?
|
||||||
|
let plainBody: String
|
||||||
|
let formattedBody: AttributedString?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
CompoundIcon(\.threads, size: .xSmall, relativeTo: .compound.bodyXS)
|
||||||
|
.foregroundColor(.compound.iconSecondary)
|
||||||
|
|
||||||
|
LoadableAvatarImage(url: sender?.avatarURL,
|
||||||
|
name: sender?.displayName,
|
||||||
|
contentID: sender?.id,
|
||||||
|
avatarSize: .user(on: .threadSummary),
|
||||||
|
mediaProvider: context.mediaProvider)
|
||||||
|
|
||||||
|
Text(sender?.disambiguatedDisplayName ?? senderID)
|
||||||
|
.font(.compound.bodyXSSemibold)
|
||||||
|
.foregroundColor(.compound.textPrimary)
|
||||||
|
.accessibilityLabel(L10n.commonInReplyTo(sender?.disambiguatedDisplayName ?? senderID))
|
||||||
|
|
||||||
|
Text(context.viewState.buildMessagePreview(formattedBody: formattedBody, plainBody: plainBody))
|
||||||
|
.font(.compound.bodyXS)
|
||||||
|
.foregroundColor(.compound.textSecondary)
|
||||||
|
.tint(.compound.textLinkExternal)
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4.0)
|
||||||
|
.padding(.horizontal, 8.0)
|
||||||
|
.background(Color.compound.bgSubtlePrimary)
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TimelineThreadSummaryView_Previews: PreviewProvider, TestablePreview {
|
||||||
|
static let viewModel = TimelineViewModel.mock
|
||||||
|
|
||||||
|
static let attributedStringWithMention = {
|
||||||
|
var attributedString = AttributedString("To be replaced")
|
||||||
|
attributedString.userID = "@alice:matrix.org"
|
||||||
|
return attributedString
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let attributedStringWithAtRoomMention = {
|
||||||
|
var attributedString = AttributedString("to be replaced")
|
||||||
|
attributedString.allUsersMention = true
|
||||||
|
return attributedString
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let attributedStringWithRoomAliasMention = {
|
||||||
|
var attributedString = AttributedString("to be replaced")
|
||||||
|
attributedString.roomAlias = "#room:matrix.org"
|
||||||
|
return attributedString
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let attributedStringWithRoomIDMention = {
|
||||||
|
var attributedString = AttributedString("to be replaced")
|
||||||
|
attributedString.roomID = "!room:matrix.org"
|
||||||
|
return attributedString
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let attributedStringWithEventOnRoomIDMention = {
|
||||||
|
var attributedString = AttributedString("to be replaced")
|
||||||
|
attributedString.eventOnRoomID = .init(roomID: "!room:matrix.org", eventID: "$event")
|
||||||
|
return attributedString
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let attributedStringWithEventOnRoomAliasMention = {
|
||||||
|
var attributedString = AttributedString("to be replaced")
|
||||||
|
attributedString.eventOnRoomAlias = .init(alias: "#room:matrix.org", eventID: "$event")
|
||||||
|
return attributedString
|
||||||
|
}()
|
||||||
|
|
||||||
|
static var previewItems: [TimelineThreadSummaryView] {
|
||||||
|
[
|
||||||
|
TimelineThreadSummaryView(threadSummary: .notLoaded),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loading),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .error(message: "Error")),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.text(.init(body: "This is a threaded message"))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "Hello world"))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.audio(.init(filename: "audio.m4a",
|
||||||
|
caption: "Some audio",
|
||||||
|
duration: 0,
|
||||||
|
waveform: nil,
|
||||||
|
source: nil,
|
||||||
|
fileSize: nil,
|
||||||
|
contentType: nil))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.file(.init(filename: "file.txt",
|
||||||
|
caption: "Some file",
|
||||||
|
source: nil,
|
||||||
|
fileSize: nil,
|
||||||
|
thumbnailSource: nil,
|
||||||
|
contentType: nil))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.image(.init(filename: "image.jpg",
|
||||||
|
caption: "Some image",
|
||||||
|
imageInfo: .mockImage,
|
||||||
|
thumbnailInfo: .mockThumbnail))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.video(.init(filename: "video.mp4",
|
||||||
|
caption: "Some video",
|
||||||
|
videoInfo: .mockVideo,
|
||||||
|
thumbnailInfo: .mockVideoThumbnail))))),
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.location(.init(body: ""))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.voice(.init(filename: "voice-message.ogg",
|
||||||
|
caption: "Some voice message",
|
||||||
|
duration: 0,
|
||||||
|
waveform: nil,
|
||||||
|
source: nil,
|
||||||
|
fileSize: nil,
|
||||||
|
contentType: nil))))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .poll(question: "Do you like polls?"))),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .redacted)),
|
||||||
|
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithMention))))),
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithAtRoomMention))))),
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithRoomAliasMention))))),
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithRoomIDMention))))),
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomIDMention))))),
|
||||||
|
TimelineThreadSummaryView(threadSummary: .loaded(senderID: "@alice:matrix.org",
|
||||||
|
sender: .init(id: "@alice:matrix.org", displayName: "Alice"),
|
||||||
|
latestEventContent: .message(.notice(.init(body: "", formattedBody: attributedStringWithEventOnRoomAliasMention)))))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
ForEach(0..<previewItems.count, id: \.self) { index in
|
||||||
|
previewItems[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.environmentObject(viewModel.context)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ final class ComposerDraftService: ComposerDraftServiceProtocol {
|
|||||||
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> {
|
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> {
|
||||||
switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) {
|
switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) {
|
||||||
case .success(let replyDetails):
|
case .success(let replyDetails):
|
||||||
return .success(timelineItemfactory.buildReply(details: replyDetails))
|
return .success(timelineItemfactory.buildTimelineItemReply(replyDetails))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
MXLog.error("Could not load reply: \(error)")
|
MXLog.error("Could not load reply: \(error)")
|
||||||
return .failure(.failedToLoadReply)
|
return .failure(.failedToLoadReply)
|
||||||
|
|||||||
+2
@@ -13,6 +13,8 @@ struct RoomTimelineItemProperties: Hashable {
|
|||||||
var replyDetails: TimelineItemReplyDetails?
|
var replyDetails: TimelineItemReplyDetails?
|
||||||
// Whether it's part of a thread or not
|
// Whether it's part of a thread or not
|
||||||
var isThreaded = false
|
var isThreaded = false
|
||||||
|
// Information about the thread this message is the root of, if any
|
||||||
|
var threadSummary: TimelineItemThreadSummary?
|
||||||
/// Whether the item has been edited.
|
/// Whether the item has been edited.
|
||||||
var isEdited = false
|
var isEdited = false
|
||||||
/// The aggregated reactions that have been sent for this item.
|
/// The aggregated reactions that have been sent for this item.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum TimelineEventContent: Hashable {
|
||||||
|
case message(EventBasedMessageTimelineItemContentType)
|
||||||
|
case poll(question: String)
|
||||||
|
case redacted
|
||||||
|
}
|
||||||
@@ -25,9 +25,3 @@ enum TimelineItemReplyDetails: Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TimelineEventContent: Hashable {
|
|
||||||
case message(EventBasedMessageTimelineItemContentType)
|
|
||||||
case poll(question: String)
|
|
||||||
case redacted
|
|
||||||
}
|
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
|
enum TimelineItemThreadSummary: Hashable {
|
||||||
|
case notLoaded
|
||||||
|
case loading
|
||||||
|
case loaded(senderID: String, sender: TimelineItemSender, latestEventContent: TimelineEventContent)
|
||||||
|
case error(message: String)
|
||||||
|
}
|
||||||
+2
-2
@@ -18,12 +18,12 @@ struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatabl
|
|||||||
|
|
||||||
let content: LocationRoomTimelineItemContent
|
let content: LocationRoomTimelineItemContent
|
||||||
|
|
||||||
|
var properties = RoomTimelineItemProperties()
|
||||||
|
|
||||||
var body: String {
|
var body: String {
|
||||||
content.body
|
content.body
|
||||||
}
|
}
|
||||||
|
|
||||||
var properties = RoomTimelineItemProperties()
|
|
||||||
|
|
||||||
var contentType: EventBasedMessageTimelineItemContentType {
|
var contentType: EventBasedMessageTimelineItemContentType {
|
||||||
.location(content)
|
.location(content)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,8 +114,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
shouldBoost: eventItemProxy.shouldBoost,
|
shouldBoost: eventItemProxy.shouldBoost,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildTextTimelineItemContent(textMessageContent),
|
content: buildTextTimelineItemContent(textMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -136,8 +137,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
shouldBoost: eventItemProxy.shouldBoost,
|
shouldBoost: eventItemProxy.shouldBoost,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildImageTimelineItemContent(imageMessageContent),
|
content: buildImageTimelineItemContent(imageMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -158,8 +160,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
shouldBoost: eventItemProxy.shouldBoost,
|
shouldBoost: eventItemProxy.shouldBoost,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildVideoTimelineItemContent(videoMessageContent),
|
content: buildVideoTimelineItemContent(videoMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -180,8 +183,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
shouldBoost: eventItemProxy.shouldBoost,
|
shouldBoost: eventItemProxy.shouldBoost,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildAudioTimelineItemContent(audioMessageContent),
|
content: buildAudioTimelineItemContent(audioMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -201,8 +205,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildAudioTimelineItemContent(audioMessageContent),
|
content: buildAudioTimelineItemContent(audioMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -223,8 +228,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
shouldBoost: eventItemProxy.shouldBoost,
|
shouldBoost: eventItemProxy.shouldBoost,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildFileTimelineItemContent(fileMessageContent),
|
content: buildFileTimelineItemContent(fileMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -244,8 +250,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildNoticeTimelineItemContent(noticeMessageContent),
|
content: buildNoticeTimelineItemContent(noticeMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -265,8 +272,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: emoteMessageContent),
|
content: buildEmoteTimelineItemContent(senderDisplayName: eventItemProxy.sender.displayName, senderID: eventItemProxy.sender.id, messageContent: emoteMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -286,8 +294,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
content: buildLocationTimelineItemContent(locationMessageContent),
|
content: buildLocationTimelineItemContent(locationMessageContent),
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: messageContent.isEdited,
|
isEdited: messageContent.isEdited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -312,8 +321,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
imageInfo: imageInfo,
|
imageInfo: imageInfo,
|
||||||
blurhash: info.blurhash,
|
blurhash: info.blurhash,
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
|
orderedReadReceipts: buildOrderedReadReceipts(eventItemProxy.readReceipts),
|
||||||
@@ -370,8 +380,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil,
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary),
|
||||||
isEdited: edited,
|
isEdited: edited,
|
||||||
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
reactions: buildAggregatedReactions(messageLikeContent.reactions),
|
||||||
deliveryStatus: eventItemProxy.deliveryStatus,
|
deliveryStatus: eventItemProxy.deliveryStatus,
|
||||||
@@ -389,8 +400,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil))
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary)))
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
@@ -441,8 +453,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
isEditable: eventItemProxy.isEditable,
|
isEditable: eventItemProxy.isEditable,
|
||||||
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
canBeRepliedTo: eventItemProxy.canBeRepliedTo,
|
||||||
sender: eventItemProxy.sender,
|
sender: eventItemProxy.sender,
|
||||||
properties: .init(replyDetails: buildReplyToDetailsFromDetailsIfAvailable(details: messageLikeContent.inReplyTo),
|
properties: .init(replyDetails: buildTimelineItemReplyDetails(messageLikeContent.inReplyTo),
|
||||||
isThreaded: messageLikeContent.threadRoot != nil))
|
isThreaded: messageLikeContent.threadRoot != nil,
|
||||||
|
threadSummary: buildTimelineItemThreadSummary(messageLikeContent.threadSummary)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Message events content
|
// MARK: - Message events content
|
||||||
@@ -628,6 +641,46 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
shieldState.flatMap(EncryptionAuthenticity.init)
|
shieldState.flatMap(EncryptionAuthenticity.init)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func buildTimelineItemThreadSummary(_ threadSummary: MatrixRustSDK.ThreadSummary?) -> TimelineItemThreadSummary? {
|
||||||
|
guard let threadSummary else { return nil }
|
||||||
|
|
||||||
|
switch threadSummary.latestEvent() {
|
||||||
|
case .unavailable:
|
||||||
|
return .notLoaded
|
||||||
|
case .pending:
|
||||||
|
return .loading
|
||||||
|
case .ready(let senderID, let senderProfile, let content):
|
||||||
|
let sender = buildTimelineItemSender(senderID: senderID, senderProfile: senderProfile)
|
||||||
|
|
||||||
|
let latestEventContent: TimelineEventContent = switch content {
|
||||||
|
case .msgLike(let messageLikeContent):
|
||||||
|
switch messageLikeContent.kind {
|
||||||
|
case .message(let messageContent):
|
||||||
|
.message(buildMessageTimelineItemContent(messageType: messageContent.msgType,
|
||||||
|
senderID: senderID,
|
||||||
|
senderDisplayName: sender.displayName))
|
||||||
|
case .poll(let question, _, _, _, _, _, _):
|
||||||
|
.poll(question: question)
|
||||||
|
case .sticker(let body, _, _):
|
||||||
|
.message(.text(.init(body: body)))
|
||||||
|
case .redacted:
|
||||||
|
.redacted
|
||||||
|
default:
|
||||||
|
.message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
.message(.text(.init(body: L10n.commonUnsupportedEvent)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return .loaded(senderID: senderID,
|
||||||
|
sender: sender,
|
||||||
|
latestEventContent: latestEventContent)
|
||||||
|
|
||||||
|
case .error(let message):
|
||||||
|
return .error(message: message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Other Events
|
// MARK: - Other Events
|
||||||
|
|
||||||
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
private func buildUnsupportedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
|
||||||
@@ -707,7 +760,15 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
// MARK: - Reply details
|
// MARK: - Reply details
|
||||||
|
|
||||||
func buildReply(details: InReplyToDetails) -> TimelineItemReply {
|
private func buildTimelineItemReplyDetails(_ details: MatrixRustSDK.InReplyToDetails?) -> TimelineItemReplyDetails? {
|
||||||
|
guard let details else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildTimelineItemReply(details).details
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTimelineItemReply(_ details: MatrixRustSDK.InReplyToDetails) -> TimelineItemReply {
|
||||||
let isThreaded = details.event().isThreaded
|
let isThreaded = details.event().isThreaded
|
||||||
switch details.event() {
|
switch details.event() {
|
||||||
case .unavailable:
|
case .unavailable:
|
||||||
@@ -715,19 +776,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
case .pending:
|
case .pending:
|
||||||
return .init(details: .loading(eventID: details.eventId()), isThreaded: isThreaded)
|
return .init(details: .loading(eventID: details.eventId()), isThreaded: isThreaded)
|
||||||
case let .ready(timelineItem, senderID, senderProfile):
|
case let .ready(timelineItem, senderID, senderProfile):
|
||||||
let sender: TimelineItemSender
|
let sender = buildTimelineItemSender(senderID: senderID, senderProfile: senderProfile)
|
||||||
switch senderProfile {
|
|
||||||
case let .ready(displayName, isDisplayNameAmbiguous, avatarUrl):
|
|
||||||
sender = TimelineItemSender(id: senderID,
|
|
||||||
displayName: displayName,
|
|
||||||
isDisplayNameAmbiguous: isDisplayNameAmbiguous,
|
|
||||||
avatarURL: avatarUrl.flatMap(URL.init(string:)))
|
|
||||||
default:
|
|
||||||
sender = TimelineItemSender(id: senderID,
|
|
||||||
displayName: nil,
|
|
||||||
isDisplayNameAmbiguous: false,
|
|
||||||
avatarURL: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
let replyContent: TimelineEventContent
|
let replyContent: TimelineEventContent
|
||||||
|
|
||||||
@@ -735,7 +784,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
case .msgLike(let messageLikeContent):
|
case .msgLike(let messageLikeContent):
|
||||||
switch messageLikeContent.kind {
|
switch messageLikeContent.kind {
|
||||||
case .message(let messageContent):
|
case .message(let messageContent):
|
||||||
return .init(details: timelineItemReplyDetails(sender: sender, eventID: details.eventId(), messageType: messageContent.msgType), isThreaded: isThreaded)
|
let replyContent = buildMessageTimelineItemContent(messageType: messageContent.msgType,
|
||||||
|
senderID: sender.id,
|
||||||
|
senderDisplayName: sender.displayName)
|
||||||
|
return .init(details: .loaded(sender: sender,
|
||||||
|
eventID: details.eventId(),
|
||||||
|
eventContent: .message(replyContent)),
|
||||||
|
isThreaded: isThreaded)
|
||||||
case .poll(let question, _, _, _, _, _, _):
|
case .poll(let question, _, _, _, _, _, _):
|
||||||
replyContent = .poll(question: question)
|
replyContent = .poll(question: question)
|
||||||
case .sticker(let body, _, _):
|
case .sticker(let body, _, _):
|
||||||
@@ -755,45 +810,48 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildReplyToDetailsFromDetailsIfAvailable(details: InReplyToDetails?) -> TimelineItemReplyDetails? {
|
// MARK: - Helpers
|
||||||
guard let details else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildReply(details: details).details
|
private func buildTimelineItemSender(senderID: String, senderProfile: ProfileDetails?) -> TimelineItemSender {
|
||||||
|
switch senderProfile {
|
||||||
|
case let .ready(displayName, isDisplayNameAmbiguous, avatarUrl):
|
||||||
|
return TimelineItemSender(id: senderID,
|
||||||
|
displayName: displayName,
|
||||||
|
isDisplayNameAmbiguous: isDisplayNameAmbiguous,
|
||||||
|
avatarURL: avatarUrl.flatMap(URL.init(string:)))
|
||||||
|
default:
|
||||||
|
return TimelineItemSender(id: senderID,
|
||||||
|
displayName: nil,
|
||||||
|
isDisplayNameAmbiguous: false,
|
||||||
|
avatarURL: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func timelineItemReplyDetails(sender: TimelineItemSender, eventID: String, messageType: MessageType?) -> TimelineItemReplyDetails {
|
private func buildMessageTimelineItemContent(messageType: MessageType?, senderID: String, senderDisplayName: String?) -> EventBasedMessageTimelineItemContentType {
|
||||||
let replyContent: EventBasedMessageTimelineItemContentType
|
|
||||||
|
|
||||||
switch messageType {
|
switch messageType {
|
||||||
case .audio(let content):
|
case .audio(let content):
|
||||||
if content.voice != nil {
|
if content.voice != nil {
|
||||||
replyContent = .voice(buildAudioTimelineItemContent(content))
|
.voice(buildAudioTimelineItemContent(content))
|
||||||
} else {
|
} else {
|
||||||
replyContent = .audio(buildAudioTimelineItemContent(content))
|
.audio(buildAudioTimelineItemContent(content))
|
||||||
}
|
}
|
||||||
case .emote(let content):
|
case .emote(let content):
|
||||||
replyContent = .emote(buildEmoteTimelineItemContent(senderDisplayName: sender.displayName, senderID: sender.id, messageContent: content))
|
.emote(buildEmoteTimelineItemContent(senderDisplayName: senderDisplayName, senderID: senderID, messageContent: content))
|
||||||
case .file(let content):
|
case .file(let content):
|
||||||
replyContent = .file(buildFileTimelineItemContent(content))
|
.file(buildFileTimelineItemContent(content))
|
||||||
case .image(let content):
|
case .image(let content):
|
||||||
replyContent = .image(buildImageTimelineItemContent(content))
|
.image(buildImageTimelineItemContent(content))
|
||||||
case .notice(let content):
|
case .notice(let content):
|
||||||
replyContent = .notice(buildNoticeTimelineItemContent(content))
|
.notice(buildNoticeTimelineItemContent(content))
|
||||||
case .text(let content):
|
case .text(let content):
|
||||||
replyContent = .text(buildTextTimelineItemContent(content))
|
.text(buildTextTimelineItemContent(content))
|
||||||
case .video(let content):
|
case .video(let content):
|
||||||
replyContent = .video(buildVideoTimelineItemContent(content))
|
.video(buildVideoTimelineItemContent(content))
|
||||||
case .location(let content):
|
case .location(let content):
|
||||||
replyContent = .location(buildLocationTimelineItemContent(content))
|
.location(buildLocationTimelineItemContent(content))
|
||||||
case .other, .none:
|
case .other, .none:
|
||||||
replyContent = .text(.init(body: L10n.commonUnsupportedEvent))
|
.text(.init(body: L10n.commonUnsupportedEvent))
|
||||||
}
|
}
|
||||||
|
|
||||||
return .loaded(sender: sender,
|
|
||||||
eventID: eventID,
|
|
||||||
eventContent: .message(replyContent))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -11,5 +11,5 @@ import MatrixRustSDK
|
|||||||
|
|
||||||
protocol RoomTimelineItemFactoryProtocol {
|
protocol RoomTimelineItemFactoryProtocol {
|
||||||
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
|
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
|
||||||
func buildReply(details: InReplyToDetails) -> TimelineItemReply
|
func buildTimelineItemReply(_ details: MatrixRustSDK.InReplyToDetails) -> TimelineItemReply
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1001,6 +1001,12 @@ extension PreviewTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testTimelineThreadSummaryView() async throws {
|
||||||
|
for (index, preview) in TimelineThreadSummaryView_Previews._allPreviews.enumerated() {
|
||||||
|
try await assertSnapshots(matching: preview, step: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testTimelineView() async throws {
|
func testTimelineView() async throws {
|
||||||
for (index, preview) in TimelineView_Previews._allPreviews.enumerated() {
|
for (index, preview) in TimelineView_Previews._allPreviews.enumerated() {
|
||||||
try await assertSnapshots(matching: preview, step: index)
|
try await assertSnapshots(matching: preview, step: index)
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0332b38d12d948ea7891f21403c6ffd524abbf9cf3dcb9840dd75219c948fc1f
|
||||||
|
size 2542610
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f7ead73a4370302fde4c9811dcee87e318294ce1d93f3134a907db3695d2aa50
|
||||||
|
size 2540695
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0a64d7a96d7b3318cb74c6c139a70f755fdfb2c1e3e7cb01f76f1bc47eaae0c2
|
||||||
|
size 1248263
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:fe424bc4fb3bfd022030e9e6a40019be65fc6f098f8583573fe02f2a2e3f2a2e
|
||||||
|
size 1245475
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:def40a09d9521734f58296a959107a0cb5d8dd3ad8ea23dc6d01dfbf298aba93
|
||||||
|
size 215724
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:9d960cd717a6bd446e2743727248f65b8ee3550557564a94b8768f7639d65931
|
||||||
|
size 220359
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0dcebc6129401b306a56627361515c900269c7d7449361e53c3b7c0e4c4a5bb7
|
||||||
|
size 149719
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:901983ae900d41fdf9b8d733d8a2e05c919e17922e75cf1bc6c91413db82fc70
|
||||||
|
size 147986
|
||||||
@@ -56,8 +56,9 @@ class RoomEventStringBuilderTests: XCTestCase {
|
|||||||
isEdited: false,
|
isEdited: false,
|
||||||
mentions: nil)),
|
mentions: nil)),
|
||||||
reactions: [],
|
reactions: [],
|
||||||
|
inReplyTo: nil,
|
||||||
threadRoot: nil,
|
threadRoot: nil,
|
||||||
inReplyTo: nil)))),
|
threadSummary: nil)))),
|
||||||
uniqueID: .init("0"))
|
uniqueID: .init("0"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ packages:
|
|||||||
# Element/Matrix dependencies
|
# Element/Matrix dependencies
|
||||||
MatrixRustSDK:
|
MatrixRustSDK:
|
||||||
url: https://github.com/element-hq/matrix-rust-components-swift
|
url: https://github.com/element-hq/matrix-rust-components-swift
|
||||||
exactVersion: 25.04.14
|
exactVersion: 25.04.16
|
||||||
# path: ../matrix-rust-sdk
|
# path: ../matrix-rust-sdk
|
||||||
Compound:
|
Compound:
|
||||||
url: https://github.com/element-hq/compound-ios
|
url: https://github.com/element-hq/compound-ios
|
||||||
|
|||||||
Reference in New Issue
Block a user