From 1230b7d970d39fb4a3af27f17a3e5f65c30cf0f0 Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 18 Jul 2024 14:27:15 +0100 Subject: [PATCH 1/2] Refactor the timeline item menu action provider. - Move it into its own struct. - Use an item, not an ID so it doesn't randomly change. - Move permissions into the room screen view model. --- ElementX.xcodeproj/project.pbxproj | 32 +++- .../RoomScreenInteractionHandler.swift | 103 ------------- .../Screens/RoomScreen/RoomScreenModels.swift | 6 +- .../RoomScreen/RoomScreenViewModel.swift | 33 ++++- .../TimelineItemMacContextMenu.swift | 6 +- .../{ => ItemMenu}/TimelineItemMenu.swift | 138 +----------------- .../ItemMenu/TimelineItemMenuAction.swift | 138 ++++++++++++++++++ .../TimelineItemMenuActionProvider.swift | 106 ++++++++++++++ .../Screens/RoomScreen/View/RoomScreen.swift | 7 +- .../Style/TimelineItemBubbledStylerView.swift | 10 +- .../RoomTimelineItemProtocol.swift | 7 + 11 files changed, 321 insertions(+), 265 deletions(-) rename ElementX/Sources/Screens/RoomScreen/View/{ => ItemMenu}/TimelineItemMacContextMenu.swift (94%) rename ElementX/Sources/Screens/RoomScreen/View/{ => ItemMenu}/TimelineItemMenu.swift (62%) create mode 100644 ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuAction.swift create mode 100644 ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuActionProvider.swift diff --git a/ElementX.xcodeproj/project.pbxproj b/ElementX.xcodeproj/project.pbxproj index 8b14e0bb52..3ce98b89eb 100644 --- a/ElementX.xcodeproj/project.pbxproj +++ b/ElementX.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 01681E8B20AD6F0D237F2DC1 /* IdentityConfirmedScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6624240FFD32B7F0834229 /* IdentityConfirmedScreenViewModel.swift */; }; 0180C44B997EDA8D21F883AC /* RoomNotificationSettingsCustomSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B746EFA112532A7B701FB914 /* RoomNotificationSettingsCustomSectionView.swift */; }; 01B63F1A04A276B39AC17014 /* CallInviteRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A3D3CFA199FA7897364547 /* CallInviteRoomTimelineItem.swift */; }; - 020C530986D7B97631877FEF /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */; }; 020F7E70167FB2833266F2F0 /* AnalyticsSettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D39D7F513A36C9C1951DB44C /* AnalyticsSettingsScreen.swift */; }; 024E70451A7CD9E4E034D8A9 /* VoiceMessageRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B976F8B2AA654D923422 /* VoiceMessageRoomTimelineItem.swift */; }; 02A92F8F4538CECDFB4F2607 /* RoomDirectorySearchScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1562EAF6231151A675BED7A9 /* RoomDirectorySearchScreenCoordinator.swift */; }; @@ -410,6 +409,7 @@ 61A36B9BB2ADE36CEFF5E98C /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E93A1BE7D8A2EBCAD51EEB4 /* Array.swift */; }; 62418EA4E3EB597AD184AEB6 /* PillConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB8D34E94AB07128DB73D6C7 /* PillConstants.swift */; }; 627139A3D79F032BA81E3A53 /* UserSessionFlowCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA29BAE9B0F2D90E57B261C /* UserSessionFlowCoordinatorTests.swift */; }; + 62833C090D599023D92A0424 /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */; }; 62910B515BCB4B455E24D7C1 /* AdvancedSettingsScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D086854995173E897F993C26 /* AdvancedSettingsScreenViewModelProtocol.swift */; }; 6298AB0906DDD3525CD78C6B /* LoremSwiftum in Frameworks */ = {isa = PBXBuildFile; productRef = 1A6B622CCFDEFB92D9CF1CA5 /* LoremSwiftum */; }; 62A7FC3A0191BC7181AA432B /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 907FA4DE17DEA1A3738EFB83 /* AudioRecorder.swift */; }; @@ -482,6 +482,7 @@ 71AC1CAAC23403FFE847F2C9 /* ComposerToolbarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C90514BE9B8ACCBCF0AD2489 /* ComposerToolbarViewModel.swift */; }; 71B62C48B8079D49F3FBC845 /* ExpiringTaskRunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7A28A6606D58D1E38C55A /* ExpiringTaskRunnerTests.swift */; }; 71C1347F23868324A4F43940 /* NavigationModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A22A05E472533ED3C5A31B3 /* NavigationModule.swift */; }; + 71C532CDC9995236FC1B6EE6 /* TimelineItemMenuActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */; }; 733E2B19AB1FDA3B93293A28 /* AppLockSetupPINScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F275432954C8C6B1B7D966 /* AppLockSetupPINScreen.swift */; }; 7354D094A4C59B555F407FA1 /* RustTracing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 542D4F49FABA056DEEEB3400 /* RustTracing.swift */; }; 7361B011A79BF723D8C9782B /* EmojiCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C1A3D524D63815B28FA4D62 /* EmojiCategory.swift */; }; @@ -566,7 +567,6 @@ 85813D87DDD7F67A46BD9AF7 /* ImageProviderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7E8A8047B50E3607ACD354E /* ImageProviderProtocol.swift */; }; 858276B19C7C0AD4CA98EA78 /* portrait_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */; }; 8587A53DE8EF94FD796DC375 /* RoomAvatarImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEF5FE93A06F563B477F024A /* RoomAvatarImage.swift */; }; - 858C04B62166B5BAFCD20F2D /* TimelineItemMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */; }; 859E2CA2EDF343BD24DE52EB /* RoomDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6404166CBF5CC88673FF9E2 /* RoomDetails.swift */; }; 85F89F3F320F4FADCFFFE68B /* ServerSelectionScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3059CFA00C67D8787273B20 /* ServerSelectionScreenViewModel.swift */; }; 864C0D3A4077BF433DBC691F /* PollRoomTimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5281C5CDC4A712265A0B5FBF /* PollRoomTimelineItem.swift */; }; @@ -751,6 +751,7 @@ AF8BFA37791E1756EE243E08 /* SettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8F0ED874DF8C9A51B0AB6F /* SettingsScreenCoordinator.swift */; }; AFE2AB612A1460E49578D746 /* JoinRoomScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDCCD2F6B405C14B9BCE94E /* JoinRoomScreenCoordinator.swift */; }; B04E9EB589CE99C3929E817A /* HomeScreenRecoveryKeyConfirmationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05512FB13987D221B7205DE0 /* HomeScreenRecoveryKeyConfirmationBanner.swift */; }; + B0BA59A46ACCF0A3ECBBB7E0 /* TimelineItemMacContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */; }; B0CB16349B96262AA65A04AF /* Sentry in Frameworks */ = {isa = PBXBuildFile; productRef = 7731767AE437BA3BD2CC14A8 /* Sentry */; }; B1069F361E604D5436AE9FFD /* StaticLocationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B06663F7858E45882E63471 /* StaticLocationScreen.swift */; }; B13774779EA19FDD7A35A4A8 /* RoomRolesAndPermissionsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C28B70BEFD3676F11D5D51F /* RoomRolesAndPermissionsScreenCoordinator.swift */; }; @@ -1039,6 +1040,7 @@ F4996C82A4B3A5FF0C8EDD03 /* RoomListFilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06AAD6D9D3F5833E7A5A2F9 /* RoomListFilterModels.swift */; }; F50A6FCE26714E27FE5495DD /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F23B21CF15F9F4BAA0788B /* PollOptionView.swift */; }; F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; }; + F541922A5B28C995E0BDB4E7 /* TimelineItemMenuAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */; }; F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; }; F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; }; F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; }; @@ -1351,6 +1353,7 @@ 307702DD66E7DDCDD9214784 /* IdentityConfirmedScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityConfirmedScreen.swift; sourceTree = ""; }; 309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = ""; }; 30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = ""; }; + 31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; 314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = ""; }; 317F41A4B5C4F457AF710666 /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = ""; }; 31A6314FDC51DA25712D9A81 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = ""; }; @@ -1462,7 +1465,6 @@ 49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = ""; }; 49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = ""; }; 49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = ""; }; - 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = ""; }; 4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = ""; }; 4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = ""; }; 4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = ""; }; @@ -1788,9 +1790,9 @@ A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = ""; }; A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = ""; }; A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = ""; }; - A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = ""; }; A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = ""; }; + A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuActionProvider.swift; sourceTree = ""; }; A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = ""; }; A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = ""; }; A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = ""; }; @@ -1845,6 +1847,7 @@ AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = ""; }; AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = ""; }; AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = ""; }; + AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuAction.swift; sourceTree = ""; }; AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = ""; }; AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = ""; }; B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelProtocol.swift; sourceTree = ""; }; @@ -2104,6 +2107,7 @@ EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = ""; }; EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = ""; }; EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledOffsetModifier.swift; sourceTree = ""; }; + EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = ""; }; ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = ""; }; ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = ""; }; ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = ""; }; @@ -2931,6 +2935,17 @@ path = QRCodeLoginScreen; sourceTree = ""; }; + 3E69187DDC5E781EB96A7BED /* ItemMenu */ = { + isa = PBXGroup; + children = ( + 31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */, + EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */, + AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */, + A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */, + ); + path = ItemMenu; + sourceTree = ""; + }; 3EA31CC7012EA2A5653DAFC9 /* Fixtures */ = { isa = PBXGroup; children = ( @@ -3787,9 +3802,8 @@ 5221DFDF809142A2D6AC82B9 /* RoomScreen.swift */, 4552D3466B1453F287223ADA /* SwipeRightAction.swift */, 7023EB4F3B7C7D1FBA68638B /* TimelineItemDebugView.swift */, - 4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */, - A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */, 0BC588051E6572A1AF51D738 /* TimelineSenderAvatarView.swift */, + 3E69187DDC5E781EB96A7BED /* ItemMenu */, 45778D52AECD4EB99A289214 /* Polls */, 4820FFB9F4FDDFD95763D498 /* ReadReceipts */, 1D8572B713A11CFDBF009B2F /* Replies */, @@ -6585,8 +6599,10 @@ 6B05AA5D9BBCD6D8D63B80EB /* TimelineItemAccessibilityModifier.swift in Sources */, 157E5FDDF419C0B2CA7E2C28 /* TimelineItemBubbledStylerView.swift in Sources */, FBCCF1EA25A071324FCD8544 /* TimelineItemDebugView.swift in Sources */, - 020C530986D7B97631877FEF /* TimelineItemMacContextMenu.swift in Sources */, - 858C04B62166B5BAFCD20F2D /* TimelineItemMenu.swift in Sources */, + B0BA59A46ACCF0A3ECBBB7E0 /* TimelineItemMacContextMenu.swift in Sources */, + 62833C090D599023D92A0424 /* TimelineItemMenu.swift in Sources */, + F541922A5B28C995E0BDB4E7 /* TimelineItemMenuAction.swift in Sources */, + 71C532CDC9995236FC1B6EE6 /* TimelineItemMenuActionProvider.swift in Sources */, 1C815DD79B401DEBA2914773 /* TimelineItemMock.swift in Sources */, 440123E29E2F9B001A775BBE /* TimelineItemProxy.swift in Sources */, 9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */, diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift index 1172091be8..296889c090 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenInteractionHandler.swift @@ -59,8 +59,6 @@ class RoomScreenInteractionHandler { } } - private var canCurrentUserRedactOthers = false - private var canCurrentUserRedactSelf = false private var resumeVoiceMessagePlaybackAfterScrubbing = false init(roomProxy: RoomProxyProtocol, @@ -84,17 +82,12 @@ class RoomScreenInteractionHandler { self.appSettings = appSettings self.analyticsService = analyticsService pollInteractionHandler = PollInteractionHandler(analyticsService: analyticsService, roomProxy: roomProxy) - - // Set initial values for redacting from the macOS context menu. - Task { await updatePermissions() } } // MARK: Timeline Item Action Menu func displayTimelineItemActionMenu(for itemID: TimelineItemIdentifier) { Task { - await updatePermissions() - guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { // Don't show a menu for non-event based items. @@ -106,84 +99,6 @@ class RoomScreenInteractionHandler { } } - // swiftlint:disable:next cyclomatic_complexity - func timelineItemMenuActionsForItemId(_ itemID: TimelineItemIdentifier) -> TimelineItemMenuActions? { - guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), - let item = timelineItem as? EventBasedTimelineItemProtocol else { - // Don't show a context menu for non-event based items. - return nil - } - - if timelineItem is StateRoomTimelineItem { - // Don't show a context menu for state events. - return nil - } - - var debugActions: [TimelineItemMenuAction] = [] - if appSettings.viewSourceEnabled { - debugActions.append(.viewSource) - } - - if let encryptedItem = timelineItem as? EncryptedRoomTimelineItem { - switch encryptedItem.encryptionType { - case .megolmV1AesSha2(let sessionID, _): - debugActions.append(.retryDecryption(sessionID: sessionID)) - default: - break - } - - return .init(actions: [.copyPermalink], debugActions: debugActions) - } - - var actions: [TimelineItemMenuAction] = [] - - if item.canBeRepliedTo { - if let messageItem = item as? EventBasedMessageTimelineItemProtocol { - actions.append(.reply(isThread: messageItem.isThreaded)) - } else { - actions.append(.reply(isThread: false)) - } - } - - if item.isForwardable { - actions.append(.forward(itemID: itemID)) - } - - if item.isEditable { - actions.append(.edit) - } - - if item.isCopyable { - actions.append(.copy) - } - - if item.isRemoteMessage { - actions.append(.copyPermalink) - } - - if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = itemID.eventID { - actions.append(.endPoll(pollStartID: eventID)) - } - - if canRedactItem(item) { - actions.append(.redact) - } - - if !item.isOutgoing { - actions.append(.report) - } - - if item.hasFailedToSend { - actions = actions.filter(\.canAppearInFailedEcho) - } - - if item.isRedacted { - actions = actions.filter(\.canAppearInRedacted) - } - - return .init(actions: actions, debugActions: debugActions) - } - func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { @@ -602,24 +517,6 @@ class RoomScreenInteractionHandler { // MARK: - Private - private func updatePermissions() async { - if case let .success(value) = await roomProxy.canUserRedactOther(userID: roomProxy.ownUserID) { - canCurrentUserRedactOthers = value - } else { - canCurrentUserRedactOthers = false - } - - if case let .success(value) = await roomProxy.canUserRedactOwn(userID: roomProxy.ownUserID) { - canCurrentUserRedactSelf = value - } else { - canCurrentUserRedactSelf = false - } - } - - private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool { - item.isOutgoing ? canCurrentUserRedactSelf : canCurrentUserRedactOthers && !roomProxy.isDirect - } - private func buildReplyInfo(for item: EventBasedTimelineItemProtocol) -> ReplyInfo { switch item { case let messageItem as EventBasedMessageTimelineItemProtocol: diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift index 75c3cf1d27..97f852ba00 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenModels.swift @@ -161,15 +161,15 @@ struct RoomScreenViewState: BindableState { var timelineViewState: TimelineViewState // check the doc before changing this var ownUserID: String + var canCurrentUserRedactOthers = false + var canCurrentUserRedactSelf = false + var isViewSourceEnabled: Bool var canJoinCall = false var hasOngoingCall = false var bindings: RoomScreenViewStateBindings - /// A closure providing the actions to show when long pressing on an item in the timeline. - var timelineItemMenuActionProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> TimelineItemMenuActions?)? - /// A closure providing the associated audio player state for an item in the timeline. var audioPlayerStateProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> AudioPlayerState?)? } diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 8bf7674e47..4576e841a3 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -86,6 +86,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol isEncryptedOneToOneRoom: roomProxy.isEncryptedOneToOneRoom, timelineViewState: TimelineViewState(focussedEvent: focussedEventID.map { .init(eventID: $0, appearance: .immediate) }), ownUserID: roomProxy.ownUserID, + isViewSourceEnabled: appSettings.viewSourceEnabled, hasOngoingCall: roomProxy.hasOngoingCall, bindings: .init(reactionsCollapsed: [:])), imageProvider: mediaProvider) @@ -98,13 +99,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol setupSubscriptions() setupDirectRoomSubscriptionsIfNeeded() - state.timelineItemMenuActionProvider = { [weak self] itemID -> TimelineItemMenuActions? in - guard let self else { - return nil - } - - return self.roomScreenInteractionHandler.timelineItemMenuActionsForItemId(itemID) - } + // Set initial values for redacting from the macOS context menu. + Task { await updatePermissions() } state.audioPlayerStateProvider = { [weak self] itemID -> AudioPlayerState? in guard let self else { @@ -351,6 +347,20 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } + private func updatePermissions() async { + if case let .success(value) = await roomProxy.canUserRedactOther(userID: roomProxy.ownUserID) { + state.canCurrentUserRedactOthers = value + } else { + state.canCurrentUserRedactOthers = false + } + + if case let .success(value) = await roomProxy.canUserRedactOwn(userID: roomProxy.ownUserID) { + state.canCurrentUserRedactSelf = value + } else { + state.canCurrentUserRedactSelf = false + } + } + private func setupSubscriptions() { timelineController.callbacks .receive(on: DispatchQueue.main) @@ -393,6 +403,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol .weakAssign(to: \.state.showReadReceipts, on: self) .store(in: &cancellables) + appSettings.$viewSourceEnabled + .weakAssign(to: \.state.isViewSourceEnabled, on: self) + .store(in: &cancellables) + roomProxy.membersPublisher .receive(on: DispatchQueue.main) .sink { [weak self] in self?.updateMembers($0) } @@ -429,7 +443,10 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case .displayRoomMemberDetails(userID: let userID): actionsSubject.send(.displayRoomMemberDetails(userID: userID)) case .showActionMenu(let actionMenuInfo): - state.bindings.actionMenuInfo = actionMenuInfo + Task { + await self.updatePermissions() + self.state.bindings.actionMenuInfo = actionMenuInfo + } case .showDebugInfo(let debugInfo): state.bindings.debugInfo = debugInfo } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMacContextMenu.swift similarity index 94% rename from ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift rename to ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMacContextMenu.swift index d0d6b0171a..1a5477bc59 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMacContextMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMacContextMenu.swift @@ -21,14 +21,14 @@ import SwiftUI /// The contents of the context menu shown when right clicking an item in the timeline on a Mac struct TimelineItemMacContextMenu: View { let item: RoomTimelineItemProtocol - let actionProvider: (@MainActor (_ itemId: TimelineItemIdentifier) -> TimelineItemMenuActions?)? + let actionProvider: TimelineItemMenuActionProvider let send: (TimelineItemMenuAction) -> Void var body: some View { if ProcessInfo.processInfo.isiOSAppOnMac { - if let menuActions = actionProvider?(item.id) { + if let menuActions = actionProvider.makeActions() { Section { - if item.isReactable { + if !menuActions.reactions.isEmpty { if #available(iOS 17.0, *) { let reactions = (item as? EventBasedTimelineItemProtocol)?.properties.reactions ?? [] ControlGroup { diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenu.swift similarity index 62% rename from ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift rename to ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenu.swift index 8cd1752171..c658cde8d3 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenu.swift @@ -15,142 +15,8 @@ // import Compound -import SFSafeSymbols import SwiftUI -struct TimelineItemMenuActions { - let reactions: [TimelineItemMenuReaction] - let actions: [TimelineItemMenuAction] - let debugActions: [TimelineItemMenuAction] - - init?(actions: [TimelineItemMenuAction], debugActions: [TimelineItemMenuAction]) { - if actions.isEmpty, debugActions.isEmpty { - return nil - } - - self.actions = actions - self.debugActions = debugActions - reactions = [ - .init(key: "👍️", symbol: .handThumbsup), - .init(key: "👎️", symbol: .handThumbsdown), - .init(key: "🔥", symbol: .flame), - .init(key: "❤️", symbol: .heart), - .init(key: "👏", symbol: .handsClap) - ] - } - - var canReply: Bool { - for action in actions { - if case .reply = action { - return true - } - } - - return false - } -} - -struct TimelineItemMenuReaction { - let key: String - let symbol: SFSymbol -} - -enum TimelineItemMenuAction: Identifiable, Hashable { - case copy - case edit - case copyPermalink - case redact - case reply(isThread: Bool) - case forward(itemID: TimelineItemIdentifier) - case viewSource - case retryDecryption(sessionID: String) - case report - case react - case toggleReaction(key: String) - case endPoll(pollStartID: String) - - var id: Self { self } - - /// Whether the item should cancel a reply/edit occurring in the composer. - var switchToDefaultComposer: Bool { - switch self { - case .reply, .edit: - return false - default: - return true - } - } - - /// Whether the action should be shown for an item that failed to send. - var canAppearInFailedEcho: Bool { - switch self { - case .copy, .edit, .redact, .viewSource: - return true - default: - return false - } - } - - /// Whether the action should be shown for a redacted item. - var canAppearInRedacted: Bool { - switch self { - case .viewSource: - return true - default: - return false - } - } - - /// Whether or not the action is destructive. - var isDestructive: Bool { - switch self { - case .redact, .report: - return true - default: - return false - } - } - - /// The action's label. - @ViewBuilder - var label: some View { - switch self { - case .copy: - Label(L10n.actionCopy, icon: \.copy) - case .edit: - Label(L10n.actionEdit, icon: \.edit) - case .copyPermalink: - Label(L10n.actionCopyLinkToMessage, icon: \.link) - case .reply(let isThread): - Label(isThread ? L10n.actionReplyInThread : L10n.actionReply, icon: \.reply) - case .forward: - Label(L10n.actionForward, icon: \.forward) - case .redact: - Label(L10n.actionRemove, icon: \.delete) - case .viewSource: - Label(L10n.actionViewSource, icon: \.code) - case .retryDecryption: - Label(L10n.actionRetryDecryption, systemImage: "arrow.down.message") - case .report: - Label(L10n.actionReportContent, icon: \.chatProblem) - case .react: - Label(L10n.actionReact, icon: \.reactionAdd) - case .toggleReaction: - // Unused label - manually created in TimelineItemMacContextMenu. - Label(L10n.actionReact, icon: \.reactionAdd) - case .endPoll: - Label(L10n.actionEndPoll, icon: \.pollsEnd) - } - } -} - -extension RoomTimelineItemProtocol { - var isReactable: Bool { - guard let eventItem = self as? EventBasedTimelineItemProtocol else { return false } - return !eventItem.isRedacted && !eventItem.hasFailedToSend && !eventItem.hasFailedDecryption - } -} - struct TimelineItemMenu: View { @EnvironmentObject private var context: RoomScreenViewModel.Context @Environment(\.dismiss) private var dismiss @@ -171,7 +37,7 @@ struct TimelineItemMenu: View { ScrollView { VStack(alignment: .leading, spacing: 0.0) { - if item.isReactable { + if !actions.reactions.isEmpty { reactionsSection .padding(.top, 4.0) .padding(.bottom, 8.0) @@ -317,7 +183,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { @ViewBuilder static var testView: some View { if let item = RoomTimelineItemFixtures.singleMessageChunk.first as? EventBasedTimelineItemProtocol, - let actions = TimelineItemMenuActions(actions: [.copy, .edit, .reply(isThread: false), .redact], debugActions: [.viewSource]) { + let actions = TimelineItemMenuActions(isReactable: true, actions: [.copy, .edit, .reply(isThread: false), .redact], debugActions: [.viewSource]) { TimelineItemMenu(item: item, actions: actions) .environmentObject(viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuAction.swift b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuAction.swift new file mode 100644 index 0000000000..1ca42c9bdf --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuAction.swift @@ -0,0 +1,138 @@ +// +// Copyright 2024 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 SFSafeSymbols +import SwiftUI + +struct TimelineItemMenuActions { + let reactions: [TimelineItemMenuReaction] + let actions: [TimelineItemMenuAction] + let debugActions: [TimelineItemMenuAction] + + init?(isReactable: Bool, actions: [TimelineItemMenuAction], debugActions: [TimelineItemMenuAction]) { + if !isReactable, actions.isEmpty, debugActions.isEmpty { + return nil + } + + self.actions = actions + self.debugActions = debugActions + reactions = if isReactable { + [ + .init(key: "👍️", symbol: .handThumbsup), + .init(key: "👎️", symbol: .handThumbsdown), + .init(key: "🔥", symbol: .flame), + .init(key: "❤️", symbol: .heart), + .init(key: "👏", symbol: .handsClap) + ] + } else { + [] + } + } +} + +struct TimelineItemMenuReaction { + let key: String + let symbol: SFSymbol +} + +enum TimelineItemMenuAction: Identifiable, Hashable { + case copy + case edit + case copyPermalink + case redact + case reply(isThread: Bool) + case forward(itemID: TimelineItemIdentifier) + case viewSource + case retryDecryption(sessionID: String) + case report + case react + case toggleReaction(key: String) + case endPoll(pollStartID: String) + + var id: Self { self } + + /// Whether the item should cancel a reply/edit occurring in the composer. + var switchToDefaultComposer: Bool { + switch self { + case .reply, .edit: + return false + default: + return true + } + } + + /// Whether the action should be shown for an item that failed to send. + var canAppearInFailedEcho: Bool { + switch self { + case .copy, .edit, .redact, .viewSource: + return true + default: + return false + } + } + + /// Whether the action should be shown for a redacted item. + var canAppearInRedacted: Bool { + switch self { + case .viewSource: + return true + default: + return false + } + } + + /// Whether or not the action is destructive. + var isDestructive: Bool { + switch self { + case .redact, .report: + return true + default: + return false + } + } + + /// The action's label. + @ViewBuilder + var label: some View { + switch self { + case .copy: + Label(L10n.actionCopy, icon: \.copy) + case .edit: + Label(L10n.actionEdit, icon: \.edit) + case .copyPermalink: + Label(L10n.actionCopyLinkToMessage, icon: \.link) + case .reply(let isThread): + Label(isThread ? L10n.actionReplyInThread : L10n.actionReply, icon: \.reply) + case .forward: + Label(L10n.actionForward, icon: \.forward) + case .redact: + Label(L10n.actionRemove, icon: \.delete) + case .viewSource: + Label(L10n.actionViewSource, icon: \.code) + case .retryDecryption: + Label(L10n.actionRetryDecryption, systemImage: "arrow.down.message") + case .report: + Label(L10n.actionReportContent, icon: \.chatProblem) + case .react: + Label(L10n.actionReact, icon: \.reactionAdd) + case .toggleReaction: + // Unused label - manually created in TimelineItemMacContextMenu. + Label(L10n.actionReact, icon: \.reactionAdd) + case .endPoll: + Label(L10n.actionEndPoll, icon: \.pollsEnd) + } + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuActionProvider.swift new file mode 100644 index 0000000000..4b9bd964fe --- /dev/null +++ b/ElementX/Sources/Screens/RoomScreen/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -0,0 +1,106 @@ +// +// Copyright 2024 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 + +struct TimelineItemMenuActionProvider { + let timelineItem: RoomTimelineItemProtocol + let canCurrentUserRedactSelf: Bool + let canCurrentUserRedactOthers: Bool + let isDM: Bool + let isViewSourceEnabled: Bool + + // swiftlint:disable:next cyclomatic_complexity + func makeActions() -> TimelineItemMenuActions? { + guard let item = timelineItem as? EventBasedTimelineItemProtocol else { + // Don't show a context menu for non-event based items. + return nil + } + + if timelineItem is StateRoomTimelineItem { + // Don't show a context menu for state events. + return nil + } + + var debugActions: [TimelineItemMenuAction] = [] + if isViewSourceEnabled { + debugActions.append(.viewSource) + } + + if let encryptedItem = timelineItem as? EncryptedRoomTimelineItem { + switch encryptedItem.encryptionType { + case .megolmV1AesSha2(let sessionID, _): + debugActions.append(.retryDecryption(sessionID: sessionID)) + default: + break + } + + return .init(isReactable: false, actions: [.copyPermalink], debugActions: debugActions) + } + + var actions: [TimelineItemMenuAction] = [] + + if item.canBeRepliedTo { + if let messageItem = item as? EventBasedMessageTimelineItemProtocol { + actions.append(.reply(isThread: messageItem.isThreaded)) + } else { + actions.append(.reply(isThread: false)) + } + } + + if item.isForwardable { + actions.append(.forward(itemID: item.id)) + } + + if item.isEditable { + actions.append(.edit) + } + + if item.isCopyable { + actions.append(.copy) + } + + if item.isRemoteMessage { + actions.append(.copyPermalink) + } + + if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = item.id.eventID { + actions.append(.endPoll(pollStartID: eventID)) + } + + if canRedactItem(item) { + actions.append(.redact) + } + + if !item.isOutgoing { + actions.append(.report) + } + + if item.hasFailedToSend { + actions = actions.filter(\.canAppearInFailedEcho) + } + + if item.isRedacted { + actions = actions.filter(\.canAppearInRedacted) + } + + return .init(isReactable: item.isReactable, actions: actions, debugActions: debugActions) + } + + private func canRedactItem(_ item: EventBasedTimelineItemProtocol) -> Bool { + item.isOutgoing ? canCurrentUserRedactSelf : canCurrentUserRedactOthers && !isDM + } +} diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index e41cdf5fee..2fa2f77ac1 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -57,7 +57,12 @@ struct RoomScreen: View { .alert(item: $context.alertInfo) .sheet(item: $context.debugInfo) { TimelineItemDebugView(info: $0) } .sheet(item: $context.actionMenuInfo) { info in - context.viewState.timelineItemMenuActionProvider?(info.item.id).map { actions in + let actions = TimelineItemMenuActionProvider(timelineItem: info.item, + canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, + canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, + isDM: context.viewState.isEncryptedOneToOneRoom, + isViewSourceEnabled: context.viewState.isViewSourceEnabled).makeActions() + if let actions { TimelineItemMenu(item: info.item, actions: actions) .environmentObject(context) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index ab69adc8aa..65bd7d77c7 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -137,14 +137,18 @@ struct TimelineItemBubbledStylerView: View { .swipeRightAction { SwipeToReplyView(timelineItem: timelineItem) } shouldStartAction: { - context.viewState.timelineItemMenuActionProvider?(timelineItem.id)?.canReply ?? false + timelineItem.canBeRepliedTo } action: { let isThread = (timelineItem as? EventBasedMessageTimelineItemProtocol)?.isThreaded ?? false context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: .reply(isThread: isThread))) } .contextMenu { - TimelineItemMacContextMenu(item: timelineItem, - actionProvider: context.viewState.timelineItemMenuActionProvider) { action in + let provider = TimelineItemMenuActionProvider(timelineItem: timelineItem, + canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf, + canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers, + isDM: context.viewState.isEncryptedOneToOneRoom, + isViewSourceEnabled: context.viewState.isViewSourceEnabled) + TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in context.send(viewAction: .handleTimelineItemMenuAction(itemID: timelineItem.id, action: action)) } } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift index c52fbac0bb..626514fb60 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemProtocol.swift @@ -20,3 +20,10 @@ import UIKit protocol RoomTimelineItemProtocol { var id: TimelineItemIdentifier { get } } + +extension RoomTimelineItemProtocol { + var isReactable: Bool { + guard let eventItem = self as? EventBasedTimelineItemProtocol else { return false } + return !eventItem.isRedacted && !eventItem.hasFailedToSend && !eventItem.hasFailedDecryption + } +} From d0de242e4fcb6964ce79edcc9570def4985301fc Mon Sep 17 00:00:00 2001 From: Doug Date: Thu, 18 Jul 2024 14:28:31 +0100 Subject: [PATCH 2/2] Use the stable ID when redacting/editing/forwarding a message. Just like we do when fetching the item in the actions menu. --- .../Services/Timeline/TimelineProxy.swift | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index d83d377095..904c62bcbd 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -77,7 +77,7 @@ final class TimelineProxy: TimelineProxyProtocol { } func messageEventContent(for timelineItemID: TimelineItemIdentifier) async -> RoomMessageEventContentWithoutRelation? { - await timelineProvider.itemProxies.firstEventTimelineItemUsingID(timelineItemID)?.content().asMessage()?.content() + await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID)?.content().asMessage()?.content() } func paginateBackwards(requestSize: UInt16) async -> Result { @@ -158,7 +158,16 @@ final class TimelineProxy: TimelineProxyProtocol { intentionalMentions: IntentionalMentions) async -> Result { MXLog.info("Editing timeline item: \(timelineItemID)") - guard let timelineItem = await timelineProvider.itemProxies.firstEventTimelineItemUsingID(timelineItemID) else { + let timelineItem: EventTimelineItem? = if !timelineItemID.timelineID.isEmpty, + let timelineItem = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID) { + timelineItem + } else if let eventID = timelineItemID.eventID { + nil // We need to edit by event ID which was removed. + } else { + nil + } + + guard let timelineItem else { MXLog.error("Unknown timeline item: \(timelineItemID)") return .failure(.failedEditing) } @@ -184,7 +193,7 @@ final class TimelineProxy: TimelineProxyProtocol { func redact(_ timelineItemID: TimelineItemIdentifier, reason: String?) async -> Result { MXLog.info("Redacting timeline item: \(timelineItemID)") - guard let eventTimelineItem = await timelineProvider.itemProxies.firstEventTimelineItemUsingID(timelineItemID) else { + guard let eventTimelineItem = await timelineProvider.itemProxies.firstEventTimelineItemUsingStableID(timelineItemID) else { MXLog.error("Unknown timeline item: \(timelineItemID)") return .failure(.failedRedacting) } @@ -600,18 +609,15 @@ private extension MatrixRustSDK.PollKind { } extension Array where Element == TimelineItemProxy { - func firstEventTimelineItemUsingID(_ id: TimelineItemIdentifier) -> EventTimelineItem? { - var eventTimelineItemProxy: EventTimelineItemProxy? - + func firstEventTimelineItemUsingStableID(_ id: TimelineItemIdentifier) -> EventTimelineItem? { for item in self { if case let .event(eventTimelineItem) = item { - if eventTimelineItem.id == id { - eventTimelineItemProxy = eventTimelineItem - break + if eventTimelineItem.id.timelineID == id.timelineID { + return eventTimelineItem.item } } } - return eventTimelineItemProxy?.item + return nil } }