diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 91baf7a5e..a110e2b14 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 51; objects = { /* Begin PBXBuildFile section */ @@ -110,6 +110,7 @@ 2955F4C160CFD7794D819C64 /* EffectsScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024F7398C5FC12586FB10E9D /* EffectsScene.swift */; }; 29EE1791E0AFA1ABB7F23D2F /* SwiftState in Frameworks */ = {isa = PBXBuildFile; productRef = 3853B78FB8531B83936C5DA6 /* SwiftState */; }; 2A90DD14DE5C891BFA433950 /* TimelineReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */; }; + 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */; }; 2ABF11717C64054CEF2819A3 /* RoomTimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F85164F9475FF2867F71AAA /* RoomTimelineController.swift */; }; 2AD59AD5B09498EF8B3B04EC /* InvitesScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */; }; 2BA59D0AEFB4B82A2EC2A326 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 50009897F60FAE7D63EF5E5B /* Kingfisher */; }; @@ -259,7 +260,6 @@ 654E802C127B84554042903E /* AnalyticsSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */; }; 659E5B766F76FDEC1BF393A4 /* RoomDetailsEditScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E413F4CBD7BF0588F394A9DD /* RoomDetailsEditScreenViewModel.swift */; }; 65EDA77363BEDC40CDE43B43 /* InvitesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42ADEA322D2089391E049535 /* InvitesScreen.swift */; }; - 661A664C6EDF856B05519206 /* InteractiveQuickLook.swift in Sources */ = {isa = PBXBuildFile; fileRef = F562E2CBA002E8E1B6545C38 /* InteractiveQuickLook.swift */; }; 663E198678778F7426A9B27D /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FAFE1C2149E6AC8156ED2B /* Collection.swift */; }; 6713835120D94BAA8ED7E3E5 /* MessageForwardingScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59846FA04E1DBBFDD8829C2A /* MessageForwardingScreenUITests.swift */; }; 67160204A8D362BB7D4AD259 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693E16574C6F7F9FA1015A8C /* Search.swift */; }; @@ -705,6 +705,7 @@ FD762761C5D0C30E6255C3D8 /* ServerConfirmationScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA4CF2F5B4F68D02E412004 /* ServerConfirmationScreenViewModelProtocol.swift */; }; FE4593FC2A02AAF92E089565 /* ElementAnimations.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF1593DD87F974F8509BB619 /* ElementAnimations.swift */; }; FF149F0A3550A54C50ECBE7A /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843CF833BF6485B64AC87E1 /* AppRouter.swift */; }; + FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4552D3466B1453F287223ADA /* SwipeRightAction.swift */; }; FFD3E4FF948E06C7585317FC /* TimelineStyler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892E29C98C4E8182C9037F84 /* TimelineStyler.swift */; }; /* End PBXBuildFile section */ @@ -809,7 +810,7 @@ 127A57D053CE8C87B5EFB089 /* Consumable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Consumable.swift; sourceTree = ""; }; 127C8472672A5BA09EF1ACF8 /* CurrentValuePublisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentValuePublisher.swift; sourceTree = ""; }; 12F1E7F9C2BE8BB751037826 /* WaitlistScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenCoordinator.swift; sourceTree = ""; }; - 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = IntegrationTests.xctestplan; sourceTree = ""; }; + 1304D9191300873EADA52D6E /* IntegrationTests.xctestplan */ = {isa = PBXFileReference; path = IntegrationTests.xctestplan; sourceTree = ""; }; 130ED565A078F7E0B59D9D25 /* UNTextInputNotificationResponse+Creator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNTextInputNotificationResponse+Creator.swift"; sourceTree = ""; }; 13802897C7AFA360EA74C0B0 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 1423AB065857FA546444DB15 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; @@ -933,6 +934,7 @@ 44D8C8431416EB8DFEC7E235 /* ApplicationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationTests.swift; sourceTree = ""; }; 450E04B2A976CC4C8CC1807C /* EmoteRoomTimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItem.swift; sourceTree = ""; }; 4549FCB53F43DB0B278374BC /* TemplateScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateScreen.swift; sourceTree = ""; }; + 4552D3466B1453F287223ADA /* SwipeRightAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeRightAction.swift; sourceTree = ""; }; 45CDF9A107BFE6C79B58D6B5 /* RoomMembersListScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModelProtocol.swift; sourceTree = ""; }; 45D8149FDDA0315CDC553B4B /* UserNotificationCenterProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationCenterProtocol.swift; sourceTree = ""; }; 466C71A0FED9BFF287613C82 /* RoomDetailsScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenModels.swift; sourceTree = ""; }; @@ -940,7 +942,7 @@ 47111410B6E659A697D472B5 /* RoomProxyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomProxyProtocol.swift; sourceTree = ""; }; 471EB7D96AFEA8D787659686 /* EmoteRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineView.swift; sourceTree = ""; }; 47873756E45B46683D97DC32 /* LegalInformationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegalInformationScreenModels.swift; sourceTree = ""; }; - 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DesignKit; sourceTree = SOURCE_ROOT; }; + 478BE8591BD13E908EF70C0C /* DesignKit */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DesignKit; path = DesignKit; sourceTree = SOURCE_ROOT; }; 4798B3B7A1E8AE3901CEE8C6 /* FramePreferenceKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FramePreferenceKey.swift; sourceTree = ""; }; 47EBB5D698CE9A25BB553A2D /* Strings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strings.swift; sourceTree = ""; }; 47F29139BC2A804CE5E0757E /* MediaUploadPreviewScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadPreviewScreenViewModel.swift; sourceTree = ""; }; @@ -1004,6 +1006,7 @@ 612EF972F2A1800682D32C5E /* StickerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StickerRoomTimelineView.swift; sourceTree = ""; }; 622D09D4ECE759189009AEAF /* MapLibreMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapLibreMapView.swift; sourceTree = ""; }; 62A81CCC2516D9CF9322DF01 /* MediaProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaProviderTests.swift; sourceTree = ""; }; + 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveQuickLook.swift; sourceTree = ""; }; 6390A6DC140CA3D6865A66FF /* SeparatorRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeparatorRoomTimelineView.swift; sourceTree = ""; }; 63E1FF2DA52B1DE7CAEC5422 /* AnalyticsPromptScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsPromptScreenViewModel.swift; sourceTree = ""; }; 63E8A1E8EE094F570573B6E8 /* RoomDetailsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDetailsScreenViewModelProtocol.swift; sourceTree = ""; }; @@ -1103,7 +1106,7 @@ 8D6094DEAAEB388E1AE118C6 /* MockRoomTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRoomTimelineProvider.swift; sourceTree = ""; }; 8D8169443E5AC5FF71BFB3DB /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 8DC2C9E0E15C79BBDA80F0A2 /* TimelineStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStyle.swift; sourceTree = ""; }; - 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UITests.xctestplan; sourceTree = ""; }; + 8E088F2A1B9EC529D3221931 /* UITests.xctestplan */ = {isa = PBXFileReference; path = UITests.xctestplan; sourceTree = ""; }; 8E1BBA73B611EDEEA6E20E05 /* InvitesScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenModels.swift; sourceTree = ""; }; 8EC57A32ABC80D774CC663DB /* SettingsScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreenUITests.swift; sourceTree = ""; }; 8F61A0DD8243B395499C99A2 /* InvitesScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitesScreenUITests.swift; sourceTree = ""; }; @@ -1207,7 +1210,7 @@ B4CFE236419E830E8946639C /* Analytics+SwiftUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Analytics+SwiftUI.swift"; sourceTree = ""; }; B590BD4507D4F0A377FDE01A /* LoadableAvatarImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableAvatarImage.swift; sourceTree = ""; }; B5B243E7818E5E9F6A4EDC7A /* NoticeRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeRoomTimelineView.swift; sourceTree = ""; }; - B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; path = ConfettiScene.scn; sourceTree = ""; }; + B61C339A2FDDBD067FF6635C /* ConfettiScene.scn */ = {isa = PBXFileReference; path = ConfettiScene.scn; sourceTree = ""; }; B6311F21F911E23BE4DF51B4 /* ReadMarkerRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadMarkerRoomTimelineView.swift; sourceTree = ""; }; B697816AF93DA06EC58C5D70 /* WaitlistScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WaitlistScreenViewModelProtocol.swift; sourceTree = ""; }; B6E89E530A8E92EC44301CA1 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; @@ -1284,7 +1287,7 @@ CD6B0C4639E066915B5E6463 /* target.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = target.yml; sourceTree = ""; }; CDB3227C7A74B734924942E9 /* RoomSummaryProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSummaryProvider.swift; sourceTree = ""; }; CEE0E6043EFCF6FD2A341861 /* TimelineReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineReplyView.swift; sourceTree = ""; }; - CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; + CEE41494C837AA403A06A5D9 /* UnitTests.xctestplan */ = {isa = PBXFileReference; path = UnitTests.xctestplan; sourceTree = ""; }; CF48AF076424DBC1615C74AD /* AuthenticationServiceProxy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationServiceProxy.swift; sourceTree = ""; }; D071F86CD47582B9196C9D16 /* UserDiscoverySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDiscoverySection.swift; sourceTree = ""; }; D09A267106B9585D3D0CFC0D /* ClientError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientError.swift; sourceTree = ""; }; @@ -1352,7 +1355,7 @@ ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED044D00F2176681CC02CD54 /* HomeScreenRoomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenRoomCell.swift; sourceTree = ""; }; ED1D792EB82506A19A72C8DE /* RoomTimelineItemProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomTimelineItemProtocol.swift; sourceTree = ""; }; - ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; lastKnownFileType = file; path = message.caf; sourceTree = ""; }; + ED482057AE39D5C6D9C5F3D8 /* message.caf */ = {isa = PBXFileReference; path = message.caf; sourceTree = ""; }; ED983D4DCA5AFA6E1ED96099 /* StateRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateRoomTimelineView.swift; sourceTree = ""; }; EDAA4472821985BF868CC21C /* ServerSelectionViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerSelectionViewModelTests.swift; sourceTree = ""; }; EE378083653EF0C9B5E9D580 /* EmoteRoomTimelineItemContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmoteRoomTimelineItemContent.swift; sourceTree = ""; }; @@ -1373,7 +1376,6 @@ F4548A9BDE5CB3AB864BCA9F /* EffectsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EffectsView.swift; sourceTree = ""; }; F506C6ADB1E1DA6638078E11 /* UITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5311C989EC15B4C2D699025 /* StaticLocationScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StaticLocationScreenViewModel.swift; sourceTree = ""; }; - F562E2CBA002E8E1B6545C38 /* InteractiveQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveQuickLook.swift; sourceTree = ""; }; F57C8022B8A871A1DCD1750A /* UserIndicatorToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserIndicatorToastView.swift; sourceTree = ""; }; F72EFC8C634469F9262659C7 /* NSItemProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProvider.swift; sourceTree = ""; }; F73FF1A33198F5FAE9D34B1F /* FormattedBodyText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormattedBodyText.swift; sourceTree = ""; }; @@ -1956,7 +1958,7 @@ 448435400B561C40E514BE1C /* FilePreviewScreen */ = { isa = PBXGroup; children = ( - F562E2CBA002E8E1B6545C38 /* InteractiveQuickLook.swift */, + 638A81B97D51591D0FCFA598 /* InteractiveQuickLook.swift */, ); path = FilePreviewScreen; sourceTree = ""; @@ -2467,6 +2469,7 @@ AA85B02533375D19744EAA46 /* RoomAttachmentPicker.swift */, 422724361B6555364C43281E /* RoomHeaderView.swift */, 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, + 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */, A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */, 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */, @@ -4072,7 +4075,6 @@ 02D8DF8EB7537EB4E9019DDB /* EventBasedTimelineItemProtocol.swift in Sources */, 63E46D18B91D08E15FC04125 /* ExpiringTaskRunner.swift in Sources */, 5F06AD3C66884CE793AE6119 /* FileManager.swift in Sources */, - 661A664C6EDF856B05519206 /* InteractiveQuickLook.swift in Sources */, D33AC79A50DFC26D2498DD28 /* FileRoomTimelineItem.swift in Sources */, 37D789F24199B32E3FD1AA7B /* FileRoomTimelineItemContent.swift in Sources */, 1F04C63D4FA95948E3F52147 /* FileRoomTimelineView.swift in Sources */, @@ -4102,6 +4104,7 @@ B796A25F282C0A340D1B9C12 /* ImageRoomTimelineItemContent.swift in Sources */, D5EA4C6C80579279770D5804 /* ImageRoomTimelineView.swift in Sources */, B6048166B4AA4CEFEA9B77A6 /* InfoPlistReader.swift in Sources */, + 2AAB2A77F1762A2648078A30 /* InteractiveQuickLook.swift in Sources */, C4D2BCAA54E2C62B94B24AF4 /* InviteUsersScreen.swift in Sources */, E27C4D1A1F8BB77CA790B403 /* InviteUsersScreenCoordinator.swift in Sources */, C26DB49C06C00B5DF1A991A5 /* InviteUsersScreenModels.swift in Sources */, @@ -4339,6 +4342,7 @@ 2F94054F50E312AF30BE07F3 /* String.swift in Sources */, A7D48E44D485B143AADDB77D /* Strings+Untranslated.swift in Sources */, 066A1E9B94723EE9F3038044 /* Strings.swift in Sources */, + FF34BF2AF731340AF9414A18 /* SwipeRightAction.swift in Sources */, E290C78E7F09F47FD2662986 /* Task.swift in Sources */, 1555A7643D85187D4851040C /* TemplateScreen.swift in Sources */, 47305C0911C9E1AA774A4000 /* TemplateScreenCoordinator.swift in Sources */, diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift index cedd0e1b7..0f80c9d26 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/LongPressWithFeedback.swift @@ -49,7 +49,7 @@ extension View { } } -struct FakeContextMenu_Previews: PreviewProvider { +struct LongPressWithFeedback_Previews: PreviewProvider { static var previews: some View { Preview() } struct Preview: View { diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index 5746fe1bc..b6b6d4cb5 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -115,6 +115,13 @@ struct TimelineItemBubbledStylerView: View { .longPressWithFeedback { context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id)) } + .swipeRightAction { + Image(systemName: "arrowshape.turn.up.left") + } shouldStartAction: { + context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false + } action: { + context.send(viewAction: .timelineItemMenuAction(itemID: timelineItem.id, action: .reply)) + } .padding(.top, messageBubbleTopPadding) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift index a1c429860..5190bfe7d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemPlainStylerView.swift @@ -71,13 +71,15 @@ struct TimelineItemPlainStylerView: View { } // We need a tap gesture before this long one so that it doesn't // steal away the gestures from the scroll view - .onLongPressGesture(minimumDuration: 0.25) { + .longPressWithFeedback { context.send(viewAction: .timelineItemMenu(itemID: timelineItem.id)) - feedbackGenerator.impactOccurred() - } onPressingChanged: { pressing in - if pressing { - feedbackGenerator.prepare() - } + } + .swipeRightAction { + Image(systemName: "arrowshape.turn.up.left") + } shouldStartAction: { + context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false + } action: { + context.send(viewAction: .timelineItemMenuAction(itemID: timelineItem.id, action: .reply)) } } diff --git a/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift b/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift new file mode 100644 index 000000000..46b718406 --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift @@ -0,0 +1,149 @@ +// +// 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 SwipeRightAction: ViewModifier { + private let actionThreshold = 50.0 + private let xOffsetThreshold = 100.0 + private let swipeThreshold = 1000.0 + + private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) + + @State private var canStartAction = false + @GestureState private var dragGestureActive = false + + @State private var hasReachedActionThreshold = false + @State private var xOffset = 0.0 + + /// The view to be shown on the left side of the content + let label: () -> Label + /// Defer computing whether an action is available until the gesture is started + let shouldStartAction: () -> Bool + /// Callback for when the dragged past the action threshold + let action: () -> Void + + func body(content: Content) -> some View { + content + .offset(x: xOffset, y: 0.0) + .animation(.interactiveSpring().speed(0.5), value: xOffset) + .gesture(DragGesture() + .updating($dragGestureActive) { _, state, _ in + // Available actions should be computed on the fly so we use a gesture state change + // to ask whether the move should be started or not. + state = true + } + .onChanged { value in + guard canStartAction else { + return + } + + // We want to add a spring like behavior to the drag in which the view + // moves slower the more it's dragged. We use a circular easing function + // to generate those values up to the `swipeThreshold` + // The final translation will be between 0 and `swipeThreshold` with the action being enabled from + // `actionThreshold` onwards + let screenWidthNormalisedTranslation = max(0.0, min(value.translation.width, swipeThreshold)) / swipeThreshold + let easedTranslation = circularEaseOut(screenWidthNormalisedTranslation) + xOffset = easedTranslation * xOffsetThreshold + + if xOffset > actionThreshold { + if !hasReachedActionThreshold { + feedbackGenerator.impactOccurred() + hasReachedActionThreshold = true + } + } else { + hasReachedActionThreshold = false + } + } + .onEnded { _ in + if xOffset > actionThreshold { + action() + } + + xOffset = 0.0 + } + ) + .onChange(of: dragGestureActive, perform: { value in + if value == true { + if shouldStartAction() { + feedbackGenerator.prepare() + canStartAction = true + } + } + }) + .overlay(alignment: .leading) { + // We want the action icon to follow the view translation and gradually fade in + label() + .opacity(xOffset / 50) + .animation(.interactiveSpring().speed(0.5), value: xOffset) + .offset(x: -actionThreshold + min(xOffset, actionThreshold), y: 0.0) + } + } + + /// Used to compute the horizontal translation amount. + /// The more it's dragged the less it moves on a circular ease out curve + private func circularEaseOut(_ value: Double) -> Double { + sqrt((2 - value) * value) + } +} + +extension View { + func swipeRightAction(label: @escaping () -> some View, + shouldStartAction: @escaping () -> Bool, + action: @escaping () -> Void) -> some View { + modifier(SwipeRightAction(label: label, shouldStartAction: shouldStartAction, action: action)) + } +} + +struct SwipeRightAction_Previews: PreviewProvider { + static var previews: some View { Preview() } + + struct Preview: View { + @State private var isPresentingSheet = false + + var body: some View { + NavigationStack { + ScrollView { + VStack(alignment: .leading) { + mockBubble("This is a message from somebody with a couple of lines of text.") + .swipeRightAction { + Image(systemName: "flame") + } shouldStartAction: { + true + } action: { + isPresentingSheet = true + } + } + .padding() + } + .navigationTitle("Work chat") + .navigationBarTitleDisplayMode(.inline) + } + .sheet(isPresented: $isPresentingSheet) { + Text("Action triggered!") + .presentationDetents([.medium]) + } + } + + func mockBubble(_ body: String) -> some View { + Text(body) + .padding(.horizontal, 12) + .padding(.vertical, 4) + .background(Color.compound._bgBubbleOutgoing, in: RoundedRectangle(cornerRadius: 12)) + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift index 102b6bb80..18090a796 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift @@ -28,6 +28,16 @@ struct TimelineItemMenuActions { self.actions = actions self.debugActions = debugActions } + + var canReply: Bool { + for action in actions { + if case .reply = action { + return true + } + } + + return false + } } enum TimelineItemMenuAction: Identifiable, Hashable {