diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 7e638f22e6..d119c9fc7f 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -354,16 +354,16 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol var actions: [TimelineItemMenuAction] = [ .reply ] - - if timelineItem is EventBasedMessageTimelineItemProtocol { + + if item.isMessage { actions.append(.forward(itemID: itemId)) } - + if item.isEditable { actions.append(.edit) } - - if timelineItem is EventBasedMessageTimelineItemProtocol { + + if item.isMessage { actions.append(.copy) } @@ -381,7 +381,15 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol case let .megolmV1AesSha2(sessionID) = item.encryptionType { debugActions.append(.retryDecryption(sessionID: sessionID)) } - + + if item.hasFailedToSend { + actions = actions.filter(\.canAppearInFailedEcho) + } + + if item.isRedacted { + actions = actions.filter(\.canAppearInRedacted) + } + return .init(actions: actions, debugActions: debugActions) } @@ -416,7 +424,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } case .redact: Task { - await timelineController.redact(itemID) + if eventTimelineItem.hasFailedToSend, + let transactionID = eventTimelineItem.properties.transactionID { + await timelineController.cancelSend(transactionID) + } else { + await timelineController.redact(itemID) + } } case .reply: state.bindings.composerFocused = true diff --git a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift index dd87a7c4b3..5746fe1bcb 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Style/TimelineItemBubbledStylerView.swift @@ -151,7 +151,7 @@ struct TimelineItemBubbledStylerView: View { @ViewBuilder var interactiveLocalizedSendInfo: some View { - if timelineItem.properties.deliveryStatus == .sendingFailed { + if timelineItem.hasFailedToSend { backgroundedLocalizedSendInfo .onTapGesture { context.sendFailedConfirmationDialogInfo = .init(transactionID: timelineItem.properties.transactionID) @@ -186,12 +186,12 @@ struct TimelineItemBubbledStylerView: View { Text(timelineItem.timestamp) } - if timelineItem.properties.deliveryStatus == .sendingFailed { + if timelineItem.hasFailedToSend { Image(systemName: "exclamationmark.circle.fill") } } .font(.compound.bodyXS) - .foregroundColor(timelineItem.properties.deliveryStatus == .sendingFailed ? .compound.textCriticalPrimary : .compound.textSecondary) + .foregroundColor(timelineItem.hasFailedToSend ? .compound.textCriticalPrimary : .compound.textSecondary) .padding(.bottom, shouldFillBubble ? 0 : -4) } diff --git a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineViewProtocol.swift b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineViewProtocol.swift index 1d78c40740..caa5749d09 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineViewProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/Timeline/TextBasedRoomTimelineViewProtocol.swift @@ -32,7 +32,7 @@ extension TextBasedRoomTimelineViewProtocol { } // To account for the extra spacing created by the alert icon - if timelineItem.properties.deliveryStatus == .sendingFailed { + if timelineItem.hasFailedToSend { whiteSpaces += 3 } diff --git a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift index 731a49c5eb..e81f49ad3d 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/TimelineItemMenu.swift @@ -51,6 +51,24 @@ enum TimelineItemMenuAction: Identifiable, Hashable { return true } } + + var canAppearInFailedEcho: Bool { + switch self { + case .copy, .redact, .viewSource: + return true + default: + return false + } + } + + var canAppearInRedacted: Bool { + switch self { + case .viewSource: + return true + default: + return false + } + } } public struct TimelineItemMenu: View { @@ -70,17 +88,21 @@ public struct TimelineItemMenu: View { ScrollView { VStack(alignment: .leading, spacing: 0.0) { - reactionsSection - .padding(.top, 4.0) - .padding(.bottom, 8.0) - - Divider() - .background(Color.compound.bgSubtlePrimary) - - viewsForActions(actions.actions) - - Divider() - .background(Color.compound.bgSubtlePrimary) + if !item.isRedacted, !item.hasFailedToSend { + reactionsSection + .padding(.top, 4.0) + .padding(.bottom, 8.0) + + Divider() + .background(Color.compound.bgSubtlePrimary) + } + + if !actions.actions.isEmpty { + viewsForActions(actions.actions) + + Divider() + .background(Color.compound.bgSubtlePrimary) + } viewsForActions(actions.debugActions) } @@ -238,7 +260,7 @@ public struct TimelineItemMenu: View { struct TimelineItemMenu_Previews: PreviewProvider { static let viewModel = RoomScreenViewModel.mock - + static var previews: some View { VStack { if let item = RoomTimelineItemFixtures.singleMessageChunk.first as? EventBasedTimelineItemProtocol, diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift index c96f535b5b..6ecb3252a5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockRoomTimelineController.swift @@ -64,6 +64,8 @@ class MockRoomTimelineController: RoomTimelineControllerProtocol { func editMessage(_ newMessage: String, original itemID: String) async { } func redact(_ itemID: String) async { } + + func cancelSend(_ transactionID: String) async { } func debugInfo(for itemID: String) -> TimelineItemDebugInfo { .init(model: "Mock debug description", originalJSON: nil, latestEditJSON: nil) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift index 05a11a5960..b5c562bcd9 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineController.swift @@ -185,6 +185,11 @@ class RoomTimelineController: RoomTimelineControllerProtocol { MXLog.error("Failed redacting message with error: \(error)") } } + + func cancelSend(_ transactionID: String) async { + MXLog.info("Cancelling send in \(roomID)") + await roomProxy.cancelSend(transactionID: transactionID) + } // Handle this parallel to the timeline items so we're not forced // to bundle the Rust side objects within them diff --git a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift index 5f07880b92..fb2bca6895 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/RoomTimelineControllerProtocol.swift @@ -57,6 +57,8 @@ protocol RoomTimelineControllerProtocol { func toggleReaction(_ reaction: String, to itemID: String) async func redact(_ itemID: String) async + + func cancelSend(_ transactionID: String) async func debugInfo(for itemID: String) -> TimelineItemDebugInfo diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index 77d49da27c..8fa59a4ce3 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -33,4 +33,16 @@ extension EventBasedTimelineItemProtocol { var description: String { "\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)" } + + var hasFailedToSend: Bool { + properties.deliveryStatus == .sendingFailed + } + + var isMessage: Bool { + self is EventBasedMessageTimelineItemProtocol + } + + var isRedacted: Bool { + self is RedactedRoomTimelineItem + } } diff --git a/changelog.d/1151.change b/changelog.d/1151.change new file mode 100644 index 0000000000..9864683327 --- /dev/null +++ b/changelog.d/1151.change @@ -0,0 +1 @@ +Filter out some message actions and reactions for failed local echoes and redacted messages. \ No newline at end of file