mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 10:33:45 +00:00
Reaction summary and flow layout for reactions (#1158)
* Add reaction summary view * Add avatars and display names and flowlayout for reactions - Add avatars to summary view - Add display names to summary view - Add flow layout to reaction buttons as per designs(long press on scolling buttons is difficult to achieve so prioritised this). * Add and use SwiftUI-Flow for reaction layout. * Correct Swift-Flow inclusion via xcodegen * Remove own FlowLayout implementation and Fix unit tests * Improve naming and correct spacing on summary view as per design. * Fix safe area insets on summary view bottom sheet * Scroll to selected reaction in the case that it is offscreen when the sheet is displayed. * Add more reactions to UITests to ensure the new flow layout is displayed(wraps on to second line). * Fix snapshot image. * Address review comments. * Use onLongPressGesture for reactions also to fix double actions bug and for consistency. * Respect RTL languages. * Fix unit test.
This commit is contained in:
@@ -146,6 +146,7 @@
|
|||||||
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; };
|
368C8758FCD079E6AAA18C2C /* NoticeRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */; };
|
||||||
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; };
|
36AC963F2F04069B7FF1AA0C /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */; };
|
||||||
36AD4DD4C798E22584ED3200 /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; };
|
36AD4DD4C798E22584ED3200 /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = A05AF81DDD14AD58CB0E1B9B /* Version */; };
|
||||||
|
36CD6E11B37396E14F032CB6 /* Flow in Frameworks */ = {isa = PBXBuildFile; productRef = 4D7E6BFC89715FC3CF0349D0 /* Flow */; };
|
||||||
37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; };
|
37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 216F0DDC98F2A2C162D09C28 /* FileRoomTimelineItemContent.swift */; };
|
||||||
38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; };
|
38546A6010A2CF240EC9AF73 /* BindableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EA1D2CBAEA5D0BD00B90D1B /* BindableState.swift */; };
|
||||||
38896D54D6D675534E606195 /* RoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */; };
|
38896D54D6D675534E606195 /* RoomTimelineControllerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6FCC416A3BFE73DF7B3E6BF /* RoomTimelineControllerFactory.swift */; };
|
||||||
@@ -213,6 +214,7 @@
|
|||||||
520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */; };
|
520EEDAFBC778AB0B41F2F53 /* ClientMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADE6170EFE6A161B0A68AB61 /* ClientMock.swift */; };
|
||||||
5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; };
|
5375902175B2FEA2949D7D74 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDDDDD9FE1A699D23A5E096 /* LoginScreen.swift */; };
|
||||||
53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; };
|
53A55748D5F587C9061F98BF /* ServerConfigurationScreenViewStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 277C20CDD5B64510401B6D0D /* ServerConfigurationScreenViewStateTests.swift */; };
|
||||||
|
53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */; };
|
||||||
53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; };
|
53DEF39F0C4DE02E3FC56D91 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 800631D7250B7F93195035F1 /* KeychainAccess */; };
|
||||||
53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; };
|
53F1196F9C69512306A2693F /* TextRoomTimelineItemContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28C19F54A0C4FC9AB7ABD583 /* TextRoomTimelineItemContent.swift */; };
|
||||||
5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D455BC2423D911A62ACFB2 /* NSELogger.swift */; };
|
5455147CAC63F71E48F7D699 /* NSELogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D455BC2423D911A62ACFB2 /* NSELogger.swift */; };
|
||||||
@@ -383,6 +385,7 @@
|
|||||||
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; };
|
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */; };
|
||||||
8F2FAA98457750D9D664136F /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
8F2FAA98457750D9D664136F /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = 4278261E147DB2DE5CFB7FC5 /* PostHog */; };
|
||||||
90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE40D4A5DD857AC16EED945A /* URLSession.swift */; };
|
90733645AE76FB33DAD28C2B /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE40D4A5DD857AC16EED945A /* URLSession.swift */; };
|
||||||
|
9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */; };
|
||||||
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; };
|
90DF83A6A347F7EE7EDE89EE /* AttributedStringBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */; };
|
||||||
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
90EB25D13AE6EEF034BDE9D2 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
||||||
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
|
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */; };
|
||||||
@@ -818,6 +821,7 @@
|
|||||||
1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
|
||||||
142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = "<group>"; };
|
142808B69851451AC32A2CEA /* RoomSummaryDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryDetails.swift; sourceTree = "<group>"; };
|
||||||
1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = "<group>"; };
|
1454CF3AABD242F55C8A2615 /* InviteUsersScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteUsersScreenModels.swift; sourceTree = "<group>"; };
|
||||||
|
153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsSummaryView.swift; sourceTree = "<group>"; };
|
||||||
15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = "<group>"; };
|
15A657D96779D1DEB8EF1327 /* CreateRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModel.swift; sourceTree = "<group>"; };
|
||||||
16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenUITests.swift; sourceTree = "<group>"; };
|
16037EE9E9A52AF37B7818E3 /* AnalyticsSettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenUITests.swift; sourceTree = "<group>"; };
|
||||||
16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = "<group>"; };
|
16D09C79746BDCD9173EB3A7 /* RoomDetailsEditScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsEditScreenModels.swift; sourceTree = "<group>"; };
|
||||||
@@ -1029,6 +1033,7 @@
|
|||||||
69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterSpy.swift; sourceTree = "<group>"; };
|
69219A908D7C22E6EE6689AE /* UserNotificationCenterSpy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterSpy.swift; sourceTree = "<group>"; };
|
||||||
693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
693E16574C6F7F9FA1015A8C /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
||||||
69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListViewModelTests.swift; sourceTree = "<group>"; };
|
69B63F817FE305548DB4B512 /* RoomMembersListViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
|
69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AggregatedReactionMock.swift; sourceTree = "<group>"; };
|
||||||
69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelTests.swift; sourceTree = "<group>"; };
|
69D42EE0102D2857933625DD /* CreateRoomViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRoomViewModelTests.swift; sourceTree = "<group>"; };
|
||||||
6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = "<group>"; };
|
6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = "<group>"; };
|
||||||
6A580295A56B55A856CC4084 /* InfoPlistReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = "<group>"; };
|
6A580295A56B55A856CC4084 /* InfoPlistReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoPlistReader.swift; sourceTree = "<group>"; };
|
||||||
@@ -1482,6 +1487,7 @@
|
|||||||
754602A7B2AAD443C4228ED4 /* Sentry in Frameworks */,
|
754602A7B2AAD443C4228ED4 /* Sentry in Frameworks */,
|
||||||
B0CB16349B96262AA65A04AF /* URLRouting in Frameworks */,
|
B0CB16349B96262AA65A04AF /* URLRouting in Frameworks */,
|
||||||
36AD4DD4C798E22584ED3200 /* Version in Frameworks */,
|
36AD4DD4C798E22584ED3200 /* Version in Frameworks */,
|
||||||
|
36CD6E11B37396E14F032CB6 /* Flow in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1766,6 +1772,7 @@
|
|||||||
31CE4DA53232AA534057F912 /* Mocks */ = {
|
31CE4DA53232AA534057F912 /* Mocks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
69CB8242D69B7E4D0B32E18D /* AggregatedReactionMock.swift */,
|
||||||
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */,
|
36FD673E24FBFCFDF398716A /* RoomMemberProxyMock.swift */,
|
||||||
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */,
|
1ABDE6F66532CBEB0E016F94 /* RoomProxyMock.swift */,
|
||||||
248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */,
|
248649EBA5BC33DB93698734 /* SessionVerificationControllerProxyMock.swift */,
|
||||||
@@ -3134,6 +3141,7 @@
|
|||||||
CCD48459CA34A1928EC7A26A /* Supplementary */ = {
|
CCD48459CA34A1928EC7A26A /* Supplementary */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
153726EDCE1ACBB3D466A916 /* ReactionsSummaryView.swift */,
|
||||||
D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */,
|
D5AC06FC11B6638F7BF1670E /* TimelineDeliveryStatusView.swift */,
|
||||||
6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */,
|
6A4C9547BBFEEF30AA11329B /* TimelineItemStatusView.swift */,
|
||||||
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */,
|
351E89CE2ED9B73C5CC47955 /* TimelineReactionsView.swift */,
|
||||||
@@ -3530,6 +3538,7 @@
|
|||||||
7731767AE437BA3BD2CC14A8 /* Sentry */,
|
7731767AE437BA3BD2CC14A8 /* Sentry */,
|
||||||
E9BAB8A793FE3B54CDD47102 /* URLRouting */,
|
E9BAB8A793FE3B54CDD47102 /* URLRouting */,
|
||||||
A05AF81DDD14AD58CB0E1B9B /* Version */,
|
A05AF81DDD14AD58CB0E1B9B /* Version */,
|
||||||
|
4D7E6BFC89715FC3CF0349D0 /* Flow */,
|
||||||
);
|
);
|
||||||
productName = ElementX;
|
productName = ElementX;
|
||||||
productReference = 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */;
|
productReference = 4CD6AC7546E8D7E5C73CEA48 /* ElementX.app */;
|
||||||
@@ -3643,6 +3652,7 @@
|
|||||||
9754C4B03F6255F67FC15E52 /* XCRemoteSwiftPackageReference "compound-ios" */,
|
9754C4B03F6255F67FC15E52 /* XCRemoteSwiftPackageReference "compound-ios" */,
|
||||||
C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */,
|
C13F55E4518415CB4C278E73 /* XCRemoteSwiftPackageReference "DTCoreText" */,
|
||||||
D5F7D47BBAAE0CF1DDEB3034 /* XCRemoteSwiftPackageReference "DeviceKit" */,
|
D5F7D47BBAAE0CF1DDEB3034 /* XCRemoteSwiftPackageReference "DeviceKit" */,
|
||||||
|
65398401562A467FD2FDCF20 /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */,
|
||||||
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */,
|
701C7BEF8F70F7A83E852DCC /* XCRemoteSwiftPackageReference "GZIP" */,
|
||||||
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
9A472EE0218FE7DCF5283429 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||||
395DE6AE429B7ACC7C7FE31D /* XCRemoteSwiftPackageReference "KZFileWatchers" */,
|
395DE6AE429B7ACC7C7FE31D /* XCRemoteSwiftPackageReference "KZFileWatchers" */,
|
||||||
@@ -3964,6 +3974,7 @@
|
|||||||
files = (
|
files = (
|
||||||
41F553349AF44567184822D8 /* APNSPayload.swift in Sources */,
|
41F553349AF44567184822D8 /* APNSPayload.swift in Sources */,
|
||||||
70394ECD2DCC70741538620D /* AccessibilityIdentifiers.swift in Sources */,
|
70394ECD2DCC70741538620D /* AccessibilityIdentifiers.swift in Sources */,
|
||||||
|
53C1E7F6A7D6409D89F36ED7 /* AggregatedReactionMock.swift in Sources */,
|
||||||
4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */,
|
4219391CD2351E410554B3E8 /* AggregratedReaction.swift in Sources */,
|
||||||
64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */,
|
64D05250CEDE8B604119F6E6 /* Alert.swift in Sources */,
|
||||||
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */,
|
39929D29B265C3F6606047DE /* AlignedScrollView.swift in Sources */,
|
||||||
@@ -4225,6 +4236,7 @@
|
|||||||
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */,
|
1BA04D05EBC6646958B1BE60 /* PlaceholderScreenCoordinator.swift in Sources */,
|
||||||
DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */,
|
DF504B10A4918F971A57BEF2 /* PostHogAnalyticsClient.swift in Sources */,
|
||||||
2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */,
|
2835FD52F3F618D07F799B3D /* Publisher.swift in Sources */,
|
||||||
|
9095B9E40DB5CF8BA26CE0D8 /* ReactionsSummaryView.swift in Sources */,
|
||||||
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */,
|
743790BF6A5B0577EA74AF14 /* ReadMarkerRoomTimelineItem.swift in Sources */,
|
||||||
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */,
|
8EF63DDDC1B54F122070B04D /* ReadMarkerRoomTimelineView.swift in Sources */,
|
||||||
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */,
|
91ABC91758A6E4A5FAA2E9C4 /* ReadReceipt.swift in Sources */,
|
||||||
@@ -5058,6 +5070,14 @@
|
|||||||
minimumVersion = 4.2.0;
|
minimumVersion = 4.2.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
65398401562A467FD2FDCF20 /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/tevelee/SwiftUI-Flow";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMinorVersion;
|
||||||
|
minimumVersion = 1.2.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */ = {
|
6582B5AF3F104B0F7E031E7D /* XCRemoteSwiftPackageReference "SwiftState" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/ReactKit/SwiftState";
|
repositoryURL = "https://github.com/ReactKit/SwiftState";
|
||||||
@@ -5256,6 +5276,11 @@
|
|||||||
package = 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */;
|
package = 96495DD8554E2F39D3954354 /* XCRemoteSwiftPackageReference "posthog-ios" */;
|
||||||
productName = PostHog;
|
productName = PostHog;
|
||||||
};
|
};
|
||||||
|
4D7E6BFC89715FC3CF0349D0 /* Flow */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 65398401562A467FD2FDCF20 /* XCRemoteSwiftPackageReference "SwiftUI-Flow" */;
|
||||||
|
productName = Flow;
|
||||||
|
};
|
||||||
50009897F60FAE7D63EF5E5B /* Kingfisher */ = {
|
50009897F60FAE7D63EF5E5B /* Kingfisher */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
package = D283517192CAC3E2E6920765 /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||||
|
|||||||
@@ -142,6 +142,15 @@
|
|||||||
"version" : "1.0.0"
|
"version" : "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swift-argument-parser",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-argument-parser",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "fee6933f37fde9a5e12a1e4aeaa93fe60116ff2a",
|
||||||
|
"version" : "1.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swift-case-paths",
|
"identity" : "swift-case-paths",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
@@ -205,6 +214,15 @@
|
|||||||
"version" : "6.0.1"
|
"version" : "6.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftui-flow",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/tevelee/SwiftUI-Flow",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d592b610a92869b74ede8ee4c0435a29219be9d8",
|
||||||
|
"version" : "1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"identity" : "swiftui-introspect",
|
"identity" : "swiftui-introspect",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension AggregatedReaction {
|
||||||
|
private static func mockIds(_ count: Int) -> [String] {
|
||||||
|
Array(1...count).map {
|
||||||
|
"@user\($0):matrix.org"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var alice: String {
|
||||||
|
RoomMemberProxyMock.mockAlice.userID
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mockThumbsUpHighlighted: AggregatedReaction {
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "👍", senders: [alice] + mockIds(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mockClap: AggregatedReaction {
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "👏", senders: mockIds(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mockParty: AggregatedReaction {
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🎉", senders: mockIds(20))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var mockReactions: [AggregatedReaction] {
|
||||||
|
[
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "😅", senders: [alice]),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🤷♂️", senders: mockIds(1)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🎨", senders: [alice] + mockIds(5)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🎉", senders: mockIds(8)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🤯", senders: [alice] + mockIds(14)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🫣", senders: mockIds(1)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🚀", senders: [alice] + mockIds(3)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "😇", senders: mockIds(2)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🤭", senders: [alice] + mockIds(8)),
|
||||||
|
AggregatedReaction(accountOwnerID: alice, key: "🫤", senders: mockIds(10))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,8 @@ enum RoomScreenViewAction {
|
|||||||
case handlePasteOrDrop(provider: NSItemProvider)
|
case handlePasteOrDrop(provider: NSItemProvider)
|
||||||
case tappedOnUser(userID: String)
|
case tappedOnUser(userID: String)
|
||||||
|
|
||||||
|
case reactionSummary(itemID: String, key: String)
|
||||||
|
|
||||||
case retrySend(transactionID: String?)
|
case retrySend(transactionID: String?)
|
||||||
case cancelSend(transactionID: String?)
|
case cancelSend(transactionID: String?)
|
||||||
}
|
}
|
||||||
@@ -126,6 +128,8 @@ struct RoomScreenViewStateBindings {
|
|||||||
var actionMenuInfo: TimelineItemActionMenuInfo?
|
var actionMenuInfo: TimelineItemActionMenuInfo?
|
||||||
|
|
||||||
var sendFailedConfirmationDialogInfo: SendFailedConfirmationDialogInfo?
|
var sendFailedConfirmationDialogInfo: SendFailedConfirmationDialogInfo?
|
||||||
|
|
||||||
|
var reactionSummaryInfo: ReactionSummaryInfo?
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineItemActionMenuInfo: Identifiable, Equatable {
|
struct TimelineItemActionMenuInfo: Identifiable, Equatable {
|
||||||
@@ -146,6 +150,15 @@ struct SendFailedConfirmationDialogInfo: ConfirmationDialogProtocol {
|
|||||||
let transactionID: String?
|
let transactionID: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ReactionSummaryInfo: Identifiable {
|
||||||
|
let reactions: [AggregatedReaction]
|
||||||
|
let selectedKey: String
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
selectedKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum RoomScreenErrorType: Hashable {
|
enum RoomScreenErrorType: Hashable {
|
||||||
/// A specific error message shown in an alert.
|
/// A specific error message shown in an alert.
|
||||||
case alert(String)
|
case alert(String)
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
case .displayEmojiPicker(let itemID):
|
case .displayEmojiPicker(let itemID):
|
||||||
guard let item = state.items.first(where: { $0.id == itemID }), item.isReactable else { return }
|
guard let item = state.items.first(where: { $0.id == itemID }), item.isReactable else { return }
|
||||||
callback?(.displayEmojiPicker(itemID: itemID))
|
callback?(.displayEmojiPicker(itemID: itemID))
|
||||||
|
case .reactionSummary(let itemId, let key):
|
||||||
|
showReactionSummary(for: itemId, selectedKey: key)
|
||||||
case .retrySend(let transactionID):
|
case .retrySend(let transactionID):
|
||||||
Task { await handleRetrySend(transactionID: transactionID) }
|
Task { await handleRetrySend(transactionID: transactionID) }
|
||||||
case .cancelSend(let transactionID):
|
case .cancelSend(let transactionID):
|
||||||
@@ -619,6 +621,17 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Reaction summary
|
||||||
|
|
||||||
|
private func showReactionSummary(for itemID: String, selectedKey: String) {
|
||||||
|
guard let timelineItem = timelineController.timelineItems.first(where: { $0.id == itemID }),
|
||||||
|
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.bindings.reactionSummaryInfo = .init(reactions: eventTimelineItem.properties.reactions, selectedKey: selectedKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension RoomProxyProtocol {
|
private extension RoomProxyProtocol {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import SwiftUI
|
|||||||
|
|
||||||
struct RoomScreen: View {
|
struct RoomScreen: View {
|
||||||
@ObservedObject var context: RoomScreenViewModel.Context
|
@ObservedObject var context: RoomScreenViewModel.Context
|
||||||
@State private var showReactionsMenuForItemId = ""
|
|
||||||
@State private var dragOver = false
|
@State private var dragOver = false
|
||||||
|
|
||||||
private let attachmentButtonPadding = 10.0
|
private let attachmentButtonPadding = 10.0
|
||||||
@@ -50,6 +49,10 @@ struct RoomScreen: View {
|
|||||||
.environmentObject(context)
|
.environmentObject(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.sheet(item: $context.reactionSummaryInfo) {
|
||||||
|
ReactionsSummaryView(reactions: $0.reactions, members: context.viewState.members, imageProvider: context.imageProvider, selectedReactionKey: $0.selectedKey)
|
||||||
|
.edgesIgnoringSafeArea([.bottom])
|
||||||
|
}
|
||||||
.interactiveQuickLook(item: $context.mediaPreviewItem)
|
.interactiveQuickLook(item: $context.mediaPreviewItem)
|
||||||
.track(screen: .room)
|
.track(screen: .room)
|
||||||
.task(id: context.viewState.roomId) {
|
.task(id: context.viewState.roomId) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import SwiftUI
|
|||||||
struct TimelineItemBubbledStylerView<Content: View>: View {
|
struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||||
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
@EnvironmentObject private var context: RoomScreenViewModel.Context
|
||||||
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
|
@Environment(\.timelineGroupStyle) private var timelineGroupStyle
|
||||||
|
@Environment(\.layoutDirection) var layoutDirection: LayoutDirection
|
||||||
|
|
||||||
let timelineItem: EventBasedTimelineItemProtocol
|
let timelineItem: EventBasedTimelineItemProtocol
|
||||||
@ViewBuilder let content: () -> Content
|
@ViewBuilder let content: () -> Content
|
||||||
@@ -32,6 +33,11 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
private var isTextItem: Bool { timelineItem is TextBasedRoomTimelineItem }
|
private var isTextItem: Bool { timelineItem is TextBasedRoomTimelineItem }
|
||||||
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
|
private var isEncryptedOneToOneRoom: Bool { context.viewState.isEncryptedOneToOneRoom }
|
||||||
|
|
||||||
|
var reactionsLayoutDirection: LayoutDirection {
|
||||||
|
guard timelineItem.isOutgoing else { return layoutDirection }
|
||||||
|
return layoutDirection == .leftToRight ? .rightToLeft : .leftToRight
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .trailingFirstTextBaseline) {
|
ZStack(alignment: .trailingFirstTextBaseline) {
|
||||||
VStack(alignment: alignment, spacing: -12) {
|
VStack(alignment: alignment, spacing: -12) {
|
||||||
@@ -92,10 +98,12 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
.accessibilityElement(children: .combine)
|
.accessibilityElement(children: .combine)
|
||||||
|
|
||||||
if !timelineItem.properties.reactions.isEmpty {
|
if !timelineItem.properties.reactions.isEmpty {
|
||||||
TimelineReactionsView(reactions: timelineItem.properties.reactions,
|
TimelineReactionsView(reactions: timelineItem.properties.reactions) { key in
|
||||||
alignment: alignment) { key in
|
|
||||||
context.send(viewAction: .toggleReaction(key: key, eventID: timelineItem.id))
|
context.send(viewAction: .toggleReaction(key: key, eventID: timelineItem.id))
|
||||||
|
} showReactionSummary: { key in
|
||||||
|
context.send(viewAction: .reactionSummary(itemID: timelineItem.id, key: key))
|
||||||
}
|
}
|
||||||
|
.environment(\.layoutDirection, reactionsLayoutDirection)
|
||||||
// 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 { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,9 +117,10 @@ struct TimelineItemPlainStylerView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !timelineItem.properties.reactions.isEmpty {
|
if !timelineItem.properties.reactions.isEmpty {
|
||||||
TimelineReactionsView(reactions: timelineItem.properties.reactions,
|
TimelineReactionsView(reactions: timelineItem.properties.reactions) { key in
|
||||||
alignment: .leading) { key in
|
|
||||||
context.send(viewAction: .toggleReaction(key: key, eventID: timelineItem.id))
|
context.send(viewAction: .toggleReaction(key: key, eventID: timelineItem.id))
|
||||||
|
} showReactionSummary: { key in
|
||||||
|
context.send(viewAction: .reactionSummary(itemID: timelineItem.id, key: key))
|
||||||
}
|
}
|
||||||
// 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 { }
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 New Vector Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ReactionsSummaryView: View {
|
||||||
|
let reactions: [AggregatedReaction]
|
||||||
|
let members: [String: RoomMemberState]
|
||||||
|
let imageProvider: ImageProviderProtocol?
|
||||||
|
|
||||||
|
@State var selectedReactionKey: String
|
||||||
|
|
||||||
|
var selectedReactionIndex: Int {
|
||||||
|
reactions.firstIndex(where: { $0.key == selectedReactionKey }) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
reactionButtons
|
||||||
|
sendersList
|
||||||
|
}
|
||||||
|
.presentationDetents([.medium])
|
||||||
|
.presentationBackground(Color.compound.bgCanvasDefault)
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var reactionButtons: some View {
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
ScrollViewReader { scrollView in
|
||||||
|
HStack {
|
||||||
|
ForEach(reactions, id: \.self) { reaction in
|
||||||
|
ReactionSummaryButton(reaction: reaction, highlighted: selectedReactionKey == reaction.key) { key in
|
||||||
|
withAnimation(.easeInOut) {
|
||||||
|
selectedReactionKey = key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.id(reaction.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.leading, 20)
|
||||||
|
.onAppear {
|
||||||
|
scrollView.scrollTo(selectedReactionKey, anchor: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 24)
|
||||||
|
.padding(.bottom, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sendersList: some View {
|
||||||
|
TabView(selection: $selectedReactionKey) {
|
||||||
|
ForEach(reactions, id: \.self) { reaction in
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
ForEach(reaction.senders, id: \.self) { sender in
|
||||||
|
ReactionSummarySenderView(sender: sender, member: members[sender], imageProvider: imageProvider)
|
||||||
|
.padding(.horizontal, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.tag(reaction.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ReactionSummaryButton: View {
|
||||||
|
let reaction: AggregatedReaction
|
||||||
|
let highlighted: Bool
|
||||||
|
let action: (String) -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button { action(reaction.key) } label: { label }
|
||||||
|
}
|
||||||
|
|
||||||
|
var label: some View {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text(reaction.key)
|
||||||
|
.font(.compound.headingSM)
|
||||||
|
if reaction.count > 1 {
|
||||||
|
Text(String(reaction.count))
|
||||||
|
.font(.compound.headingSM)
|
||||||
|
.foregroundColor(highlighted ? Color.compound.textOnSolidPrimary : Color.compound.textSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.background(highlighted ? Color.compound.bgActionPrimaryRest : .clear, in: Capsule())
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ReactionSummarySenderView: View {
|
||||||
|
var sender: String
|
||||||
|
var member: RoomMemberState?
|
||||||
|
let imageProvider: ImageProviderProtocol?
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
member?.displayName ?? sender
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
LoadableAvatarImage(url: member?.avatarURL,
|
||||||
|
name: displayName,
|
||||||
|
contentID: sender,
|
||||||
|
avatarSize: .user(on: .timeline),
|
||||||
|
imageProvider: imageProvider)
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(displayName)
|
||||||
|
.font(.compound.bodyMDSemibold)
|
||||||
|
Text(sender)
|
||||||
|
.font(.compound.bodySM)
|
||||||
|
.foregroundColor(.compound.textSecondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReactionsSummaryView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ReactionsSummaryView(reactions: AggregatedReaction.mockReactions,
|
||||||
|
members: [:],
|
||||||
|
imageProvider: MockMediaProvider(),
|
||||||
|
selectedReactionKey: AggregatedReaction.mockReactions[0].key)
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-25
@@ -14,30 +14,41 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Flow
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct TimelineReactionsView: View {
|
struct TimelineReactionsView: View {
|
||||||
|
@Environment(\.layoutDirection) var layoutDirection: LayoutDirection
|
||||||
let reactions: [AggregatedReaction]
|
let reactions: [AggregatedReaction]
|
||||||
let alignment: HorizontalAlignment
|
let toggleReaction: (String) -> Void
|
||||||
let action: (String) -> Void
|
let showReactionSummary: (String) -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
AlignedScrollView(alignment: alignment, showsIndicators: false) {
|
HFlow(itemSpacing: 4, rowSpacing: 4) {
|
||||||
HStack {
|
ForEach(reactions, id: \.self) { reaction in
|
||||||
ForEach(reactions, id: \.self) { reaction in
|
TimelineReactionButton(reaction: reaction,
|
||||||
TimelineReactionButton(reaction: reaction, action: action)
|
toggleReaction: toggleReaction,
|
||||||
}
|
showReactionSummary: showReactionSummary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.environment(\.layoutDirection, layoutDirection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TimelineReactionButton: View {
|
struct TimelineReactionButton: View {
|
||||||
let reaction: AggregatedReaction
|
let reaction: AggregatedReaction
|
||||||
let action: (String) -> Void
|
let toggleReaction: (String) -> Void
|
||||||
|
let showReactionSummary: (String) -> Void
|
||||||
|
|
||||||
|
@State private var didLongPress = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button { action(reaction.key) } label: { label }
|
label.onTapGesture {
|
||||||
|
toggleReaction(reaction.key)
|
||||||
|
}
|
||||||
|
.longPressWithFeedback {
|
||||||
|
showReactionSummary(reaction.key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var label: some View {
|
var label: some View {
|
||||||
@@ -79,22 +90,13 @@ struct TimelineReactionButton: View {
|
|||||||
struct TimelineReactionView_Previews: PreviewProvider {
|
struct TimelineReactionView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
TimelineReactionButton(reaction: AggregatedReaction(key: "👍", count: 5, isHighlighted: true)) { _ in }
|
TimelineReactionButton(reaction: AggregatedReaction.mockThumbsUpHighlighted) { _ in } showReactionSummary: { _ in }
|
||||||
TimelineReactionButton(reaction: AggregatedReaction(key: "👏", count: 1, isHighlighted: false)) { _ in }
|
TimelineReactionButton(reaction: AggregatedReaction.mockClap) { _ in } showReactionSummary: { _ in }
|
||||||
TimelineReactionButton(reaction: AggregatedReaction(key: "🎉", count: 20, isHighlighted: false)) { _ in }
|
TimelineReactionButton(reaction: AggregatedReaction.mockParty) { _ in } showReactionSummary: { _ in }
|
||||||
|
TimelineReactionsView(reactions: AggregatedReaction.mockReactions) { _ in } showReactionSummary: { _ in }
|
||||||
TimelineReactionsView(reactions: [
|
.environment(\.layoutDirection, .leftToRight)
|
||||||
AggregatedReaction(key: "😅", count: 1, isHighlighted: true),
|
TimelineReactionsView(reactions: AggregatedReaction.mockReactions) { _ in } showReactionSummary: { _ in }
|
||||||
AggregatedReaction(key: "🤷♂️", count: 1, isHighlighted: false),
|
.environment(\.layoutDirection, .rightToLeft)
|
||||||
AggregatedReaction(key: "🎨", count: 6, isHighlighted: true),
|
|
||||||
AggregatedReaction(key: "🎉", count: 8, isHighlighted: false),
|
|
||||||
AggregatedReaction(key: "🤯", count: 15, isHighlighted: true),
|
|
||||||
AggregatedReaction(key: "🫣", count: 1, isHighlighted: false),
|
|
||||||
AggregatedReaction(key: "🚀", count: 3, isHighlighted: true),
|
|
||||||
AggregatedReaction(key: "😇", count: 2, isHighlighted: false),
|
|
||||||
AggregatedReaction(key: "🤭", count: 9, isHighlighted: true),
|
|
||||||
AggregatedReaction(key: "🫤", count: 10, isHighlighted: false)
|
|
||||||
], alignment: .leading) { _ in }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ enum RoomTimelineItemFixtures {
|
|||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
content: .init(body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗"),
|
content: .init(body: "Let’s get lunch soon! New salad place opened up 🥗. When are y’all free? 🤗"),
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
AggregatedReaction(key: "🙌", count: 1, isHighlighted: true)
|
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me"])
|
||||||
])),
|
])),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
timestamp: "10:11 AM",
|
timestamp: "10:11 AM",
|
||||||
@@ -43,8 +43,8 @@ enum RoomTimelineItemFixtures {
|
|||||||
sender: .init(id: "", displayName: "Helena"),
|
sender: .init(id: "", displayName: "Helena"),
|
||||||
content: .init(body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/"),
|
content: .init(body: "I can be around on Wednesday. How about some 🌮 instead? Like https://www.tortilla.co.uk/"),
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
AggregatedReaction(accountOwnerID: "me", key: "🙏", senders: ["helena"]),
|
||||||
AggregatedReaction(key: "🙌", count: 2, isHighlighted: true)
|
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me", "helena", "jacob"])
|
||||||
])),
|
])),
|
||||||
SeparatorRoomTimelineItem(id: "Today", text: "Today"),
|
SeparatorRoomTimelineItem(id: "Today", text: "Today"),
|
||||||
TextRoomTimelineItem(id: UUID().uuidString,
|
TextRoomTimelineItem(id: UUID().uuidString,
|
||||||
@@ -67,8 +67,15 @@ enum RoomTimelineItemFixtures {
|
|||||||
sender: .init(id: "", displayName: "Bob"),
|
sender: .init(id: "", displayName: "Bob"),
|
||||||
content: .init(body: "New home office set up!"),
|
content: .init(body: "New home office set up!"),
|
||||||
properties: RoomTimelineItemProperties(reactions: [
|
properties: RoomTimelineItemProperties(reactions: [
|
||||||
AggregatedReaction(key: "🙏", count: 1, isHighlighted: false),
|
AggregatedReaction(accountOwnerID: "me", key: "🙏", senders: ["helena"]),
|
||||||
AggregatedReaction(key: "😁", count: 3, isHighlighted: false)
|
AggregatedReaction(accountOwnerID: "me", key: "🙌", senders: ["me", "helena", "jacob", "bob", "alice", "charlie", "dan", "erin"]),
|
||||||
|
AggregatedReaction(accountOwnerID: "me", key: "👌", senders: ["helena"]),
|
||||||
|
AggregatedReaction(accountOwnerID: "me", key: "🥳", senders: ["bob"]),
|
||||||
|
AggregatedReaction(accountOwnerID: "me", key: "🍾", senders: ["charlie"]),
|
||||||
|
AggregatedReaction(accountOwnerID: "me", key: "🎈", senders: ["helena"]),
|
||||||
|
AggregatedReaction(accountOwnerID: "me", key: "👏", senders: ["helena"]),
|
||||||
|
AggregatedReaction(accountOwnerID: "me", key: "🎉", senders: ["helena"])
|
||||||
|
|
||||||
],
|
],
|
||||||
orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil),
|
orderedReadReceipts: [ReadReceipt(userID: "alice", formattedTimestamp: nil),
|
||||||
ReadReceipt(userID: "bob", formattedTimestamp: nil),
|
ReadReceipt(userID: "bob", formattedTimestamp: nil),
|
||||||
|
|||||||
@@ -18,10 +18,22 @@ import Foundation
|
|||||||
|
|
||||||
/// Represents all reactions of the same type for a single event.
|
/// Represents all reactions of the same type for a single event.
|
||||||
struct AggregatedReaction: Hashable {
|
struct AggregatedReaction: Hashable {
|
||||||
|
/// The id of the account owner
|
||||||
|
let accountOwnerID: String
|
||||||
/// The reaction that was sent.
|
/// The reaction that was sent.
|
||||||
let key: String
|
let key: String
|
||||||
/// The number of times this reactions was sent.
|
/// The user ids of those who sent the reactions
|
||||||
let count: Int
|
let senders: [String]
|
||||||
/// Whether to highlight the reaction, indicating that the current user sent this reaction.
|
}
|
||||||
let isHighlighted: Bool
|
|
||||||
|
extension AggregatedReaction {
|
||||||
|
/// The number of times this reactions was sent.
|
||||||
|
var count: Int {
|
||||||
|
senders.count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether to highlight the reaction, indicating that the current user sent this reaction.
|
||||||
|
var isHighlighted: Bool {
|
||||||
|
senders.contains(where: { $0 == accountOwnerID })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,8 +322,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
|
|||||||
|
|
||||||
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
|
private func aggregateReactions(_ reactions: [Reaction]) -> [AggregatedReaction] {
|
||||||
reactions.map { reaction in
|
reactions.map { reaction in
|
||||||
let isHighlighted = reaction.senders.contains(where: { $0 == userID })
|
AggregatedReaction(accountOwnerID: userID, key: reaction.key, senders: reaction.senders)
|
||||||
return AggregatedReaction(key: reaction.key, count: Int(reaction.count), isHighlighted: isHighlighted)
|
}.sorted { a, b in
|
||||||
|
a.count > b.count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ targets:
|
|||||||
- package: Sentry
|
- package: Sentry
|
||||||
- package: URLRouting
|
- package: URLRouting
|
||||||
- package: Version
|
- package: Version
|
||||||
|
- package: Flow
|
||||||
|
|
||||||
sources:
|
sources:
|
||||||
- path: ../Sources
|
- path: ../Sources
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6b77e85ff9c5f5b1d888c97af89a511f788aa9e74eb0d609659c7d81e2b89b94
|
oid sha256:2876d98496585f32beb44ee4e625b4ff1f790dead3db0061e15e72eecef006f6
|
||||||
size 231654
|
size 250594
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2c6127923272006fe70a164fca243c1683d32d0227c8066f6794957567724a41
|
oid sha256:166a3ee681e30d0f0da944324e66ec945dfdc5c6ee480383c124fe4b003bb1e5
|
||||||
size 328078
|
size 337908
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:bfc0399661f579abcba48d2d003e710e1f54b235186d26d5fb7f6d3ffcbe235a
|
oid sha256:e0d851f39979376d0f7e33376a04ad803c818f4d1fb491db1b456cc1d6bd5e7b
|
||||||
size 232688
|
size 251684
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6d91a63988ebcb6be677fa4030f1cc9c0bfad8ede08f1568a2ca8c82bab4f443
|
oid sha256:b2b1b811e13e996ee452e66515936370e80d2d98fab865247721621e78c01b8e
|
||||||
size 347244
|
size 338503
|
||||||
|
|||||||
@@ -348,14 +348,13 @@ class RoomScreenViewModelTests: XCTestCase {
|
|||||||
|
|
||||||
private extension TextRoomTimelineItem {
|
private extension TextRoomTimelineItem {
|
||||||
init(text: String, sender: String, addReactions: Bool = false) {
|
init(text: String, sender: String, addReactions: Bool = false) {
|
||||||
|
let reactions = addReactions ? [AggregatedReaction(accountOwnerID: "bob", key: "🦄", senders: [sender])] : []
|
||||||
self.init(id: UUID().uuidString,
|
self.init(id: UUID().uuidString,
|
||||||
timestamp: "10:47 am",
|
timestamp: "10:47 am",
|
||||||
isOutgoing: sender == "bob",
|
isOutgoing: sender == "bob",
|
||||||
isEditable: sender == "bob",
|
isEditable: sender == "bob",
|
||||||
sender: .init(id: "@\(sender):server.com", displayName: sender),
|
sender: .init(id: "@\(sender):server.com", displayName: sender),
|
||||||
content: .init(body: text),
|
content: .init(body: text),
|
||||||
properties: RoomTimelineItemProperties(reactions: addReactions ? [
|
properties: RoomTimelineItemProperties(reactions: reactions))
|
||||||
AggregatedReaction(key: "🦄", count: 1, isHighlighted: false)
|
|
||||||
] : []))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,3 +103,6 @@ packages:
|
|||||||
Version:
|
Version:
|
||||||
url: https://github.com/mxcl/Version
|
url: https://github.com/mxcl/Version
|
||||||
minorVersion: 2.0.0
|
minorVersion: 2.0.0
|
||||||
|
Flow:
|
||||||
|
url: https://github.com/tevelee/SwiftUI-Flow
|
||||||
|
minorVersion: 1.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user