mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Fix a potential race condition when redacting a message. (#3061)
* 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. * Use the stable ID when redacting/editing/forwarding a message. Just like we do when fetching the item in the actions menu.
This commit is contained in:
parent
2fb7f65957
commit
6339fe2bf0
@ -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 = "<group>"; };
|
||||
309AD8BAE6437C31BA7157BF /* ElementCallWidgetDriver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementCallWidgetDriver.swift; sourceTree = "<group>"; };
|
||||
30ED584467DB380E3CEFB1DB /* NotificationManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManagerTests.swift; sourceTree = "<group>"; };
|
||||
31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
|
||||
314F1C79850BE46E8ABEAFCB /* ReadReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadReceipt.swift; sourceTree = "<group>"; };
|
||||
317F41A4B5C4F457AF710666 /* PollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollView.swift; sourceTree = "<group>"; };
|
||||
31A6314FDC51DA25712D9A81 /* PillContextTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillContextTests.swift; sourceTree = "<group>"; };
|
||||
@ -1462,7 +1465,6 @@
|
||||
49E45C3DC740D3AB9A47FD32 /* SwipeToReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToReplyView.swift; sourceTree = "<group>"; };
|
||||
49E6066092ED45E36BB306F7 /* zh-Hant-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-Hant-TW"; path = "zh-Hant-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
49E751D7EDB6043238111D90 /* UNNotificationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UNNotificationRequest.swift; sourceTree = "<group>"; };
|
||||
4A4AD793D50748F8997E5B15 /* TimelineItemMacContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMacContextMenu.swift; sourceTree = "<group>"; };
|
||||
4A5B4CD611DE7E94F5BA87B2 /* AppLockTimerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockTimerTests.swift; sourceTree = "<group>"; };
|
||||
4AB7D7DAAAF662DED9D02379 /* MockMediaLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockMediaLoader.swift; sourceTree = "<group>"; };
|
||||
4B2D4EEBE8C098BBADD10939 /* SecureBackupKeyBackupScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBackupKeyBackupScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
@ -1788,9 +1790,9 @@
|
||||
A130A2251A15A7AACC84FD37 /* RoomPollsHistoryScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollsHistoryScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
A16CD2C62CB7DB78A4238485 /* ReportContentScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportContentScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
A16D0F226B1819D017531647 /* BlockedUsersScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedUsersScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
A1BF12A5E7C76777C4BF0F2B /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = "<group>"; };
|
||||
A1C22B1B5FA3A765EADB2CC9 /* SessionVerificationStateMachineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationStateMachineTests.swift; sourceTree = "<group>"; };
|
||||
A232D9156D225BD9FD1D0C43 /* PhotoLibraryPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoLibraryPicker.swift; sourceTree = "<group>"; };
|
||||
A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuActionProvider.swift; sourceTree = "<group>"; };
|
||||
A2AC3C656E960E15B5905E05 /* UnsupportedRoomTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsupportedRoomTimelineView.swift; sourceTree = "<group>"; };
|
||||
A3B4B58B79A6FA250B24A1EC /* HomeScreenContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeScreenContent.swift; sourceTree = "<group>"; };
|
||||
A3FBD9C2B9A5479526920399 /* BugReportScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BugReportScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||
@ -1845,6 +1847,7 @@
|
||||
AEEAFB646E583655652C3D04 /* RoomStateEventStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomStateEventStringBuilderTests.swift; sourceTree = "<group>"; };
|
||||
AF042B0FB2EE88977C91E330 /* portrait_test_image.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = portrait_test_image.jpg; sourceTree = "<group>"; };
|
||||
AF25E364AE85090A70AE4644 /* AttributedStringBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedStringBuilderTests.swift; sourceTree = "<group>"; };
|
||||
AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenuAction.swift; sourceTree = "<group>"; };
|
||||
AF848B41DAF1066F3054D4A1 /* SessionVerificationScreenModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionVerificationScreenModels.swift; sourceTree = "<group>"; };
|
||||
AFEF489B8E2450E2BA1A314E /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||
B0618820D26F9871A4BBB40E /* ComposerToolbarViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerToolbarViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
@ -2104,6 +2107,7 @@
|
||||
EBEB8D9F4940E161B18FE4BC /* UITestsNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITestsNotificationCenter.swift; sourceTree = "<group>"; };
|
||||
EC589E641AE46EFB2962534D /* RoomMemberDetailsViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMemberDetailsViewModelTests.swift; sourceTree = "<group>"; };
|
||||
EC5D7DA665E1F5F509C994C7 /* ScaledOffsetModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScaledOffsetModifier.swift; sourceTree = "<group>"; };
|
||||
EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemMenu.swift; sourceTree = "<group>"; };
|
||||
ECD5FCBA169B6A82F501CA1B /* AnalyticsSettingsScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSettingsScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||
ECF79FB25E2D4BD6F50CE7C9 /* RoomMembersListScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomMembersListScreenViewModel.swift; sourceTree = "<group>"; };
|
||||
ED003DF1B7CF40E7073A2280 /* TracingConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracingConfiguration.swift; sourceTree = "<group>"; };
|
||||
@ -2931,6 +2935,17 @@
|
||||
path = QRCodeLoginScreen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3E69187DDC5E781EB96A7BED /* ItemMenu */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
31352FFC2EF2C353CB7EA376 /* TimelineItemMacContextMenu.swift */,
|
||||
EC8D3544DE3FABD958BA8F19 /* TimelineItemMenu.swift */,
|
||||
AF64B3A815D04325F1980E02 /* TimelineItemMenuAction.swift */,
|
||||
A2751266F17A5BF25DA9227E /* TimelineItemMenuActionProvider.swift */,
|
||||
);
|
||||
path = ItemMenu;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
@ -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:
|
||||
|
@ -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?)?
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
@ -137,14 +137,18 @@ struct TimelineItemBubbledStylerView<Content: View>: 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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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<Void, TimelineProxyError> {
|
||||
@ -158,7 +158,16 @@ final class TimelineProxy: TimelineProxyProtocol {
|
||||
intentionalMentions: IntentionalMentions) async -> Result<Void, TimelineProxyError> {
|
||||
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<Void, TimelineProxyError> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user