From e51d869fdc624d3323c0447d6ba87509e25d4cde Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Fri, 18 Sep 2026 15:39:52 +0300 Subject: [PATCH] Adopt the new send queue interfaces --- .../Mocks/Generated/GeneratedMocks.swift | 188 ++++++++++++++++++ .../Screens/Timeline/TimelineModels.swift | 2 +- .../Screens/Timeline/TimelineViewModel.swift | 29 ++- .../TimelineController.swift | 47 +++++ .../TimelineControllerProtocol.swift | 6 + .../Services/Timeline/TimelineItemProxy.swift | 17 +- .../Services/Timeline/TimelineProxy.swift | 30 +++ .../Timeline/TimelineProxyProtocol.swift | 6 + .../Sources/TimelineViewModelTests.swift | 2 +- 9 files changed, 308 insertions(+), 19 deletions(-) diff --git a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift index c2caf235a..58d0b0abc 100644 --- a/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift +++ b/ElementX/Sources/Mocks/Generated/GeneratedMocks.swift @@ -12506,6 +12506,110 @@ nonisolated class TimelineControllerMock: TimelineControllerProtocol, @unchecked return sendHandleForReturnValue } } + //MARK: - pendingSendTarget + + private let pendingSendTargetForCallsCountLock = NSLock() + private nonisolated(unsafe) var pendingSendTargetForUnderlyingCallsCount = 0 + var pendingSendTargetForCallsCount: Int { + get { pendingSendTargetForCallsCountLock.withLock { pendingSendTargetForUnderlyingCallsCount } } + set { pendingSendTargetForCallsCountLock.withLock { pendingSendTargetForUnderlyingCallsCount = newValue } } + } + var pendingSendTargetForCalled: Bool { + return pendingSendTargetForCallsCount > 0 + } + private let pendingSendTargetForReceivedItemIDLock = NSLock() + private nonisolated(unsafe) var pendingSendTargetForUnderlyingReceivedItemID: TimelineItemIdentifier? + var pendingSendTargetForReceivedItemID: TimelineItemIdentifier? { + get { pendingSendTargetForReceivedItemIDLock.withLock { pendingSendTargetForUnderlyingReceivedItemID } } + set { pendingSendTargetForReceivedItemIDLock.withLock { pendingSendTargetForUnderlyingReceivedItemID = newValue } } + } + private let pendingSendTargetForReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var pendingSendTargetForUnderlyingReceivedInvocations: [TimelineItemIdentifier] = [] + var pendingSendTargetForReceivedInvocations: [TimelineItemIdentifier] { + get { pendingSendTargetForReceivedInvocationsLock.withLock { pendingSendTargetForUnderlyingReceivedInvocations } } + set { pendingSendTargetForReceivedInvocationsLock.withLock { pendingSendTargetForUnderlyingReceivedInvocations = newValue } } + } + + private let pendingSendTargetForReturnValueLock = NSLock() + private nonisolated(unsafe) var pendingSendTargetForUnderlyingReturnValue: SendTarget? + var pendingSendTargetForReturnValue: SendTarget? { + get { pendingSendTargetForReturnValueLock.withLock { pendingSendTargetForUnderlyingReturnValue } } + set { pendingSendTargetForReturnValueLock.withLock { pendingSendTargetForUnderlyingReturnValue = newValue } } + } + nonisolated(unsafe) var pendingSendTargetForClosure: ((TimelineItemIdentifier) -> SendTarget?)? + + func pendingSendTarget(for itemID: TimelineItemIdentifier) -> SendTarget? { + pendingSendTargetForCallsCountLock.withLock { pendingSendTargetForUnderlyingCallsCount += 1 } + pendingSendTargetForReceivedItemID = itemID + pendingSendTargetForReceivedInvocationsLock.withLock { pendingSendTargetForUnderlyingReceivedInvocations.append(itemID) } + if let pendingSendTargetForClosure = pendingSendTargetForClosure { + return pendingSendTargetForClosure(itemID) + } else { + return pendingSendTargetForReturnValue + } + } + //MARK: - retrySend + + private let retrySendTargetCallsCountLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingCallsCount = 0 + var retrySendTargetCallsCount: Int { + get { retrySendTargetCallsCountLock.withLock { retrySendTargetUnderlyingCallsCount } } + set { retrySendTargetCallsCountLock.withLock { retrySendTargetUnderlyingCallsCount = newValue } } + } + var retrySendTargetCalled: Bool { + return retrySendTargetCallsCount > 0 + } + private let retrySendTargetReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingReceivedArguments: (itemID: TimelineItemIdentifier, target: SendTarget)? + var retrySendTargetReceivedArguments: (itemID: TimelineItemIdentifier, target: SendTarget)? { + get { retrySendTargetReceivedArgumentsLock.withLock { retrySendTargetUnderlyingReceivedArguments } } + set { retrySendTargetReceivedArgumentsLock.withLock { retrySendTargetUnderlyingReceivedArguments = newValue } } + } + private let retrySendTargetReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingReceivedInvocations: [(itemID: TimelineItemIdentifier, target: SendTarget)] = [] + var retrySendTargetReceivedInvocations: [(itemID: TimelineItemIdentifier, target: SendTarget)] { + get { retrySendTargetReceivedInvocationsLock.withLock { retrySendTargetUnderlyingReceivedInvocations } } + set { retrySendTargetReceivedInvocationsLock.withLock { retrySendTargetUnderlyingReceivedInvocations = newValue } } + } + nonisolated(unsafe) var retrySendTargetClosure: ((TimelineItemIdentifier, SendTarget) async -> Void)? + + @concurrent func retrySend(_ itemID: TimelineItemIdentifier, target: SendTarget) async { + retrySendTargetCallsCountLock.withLock { retrySendTargetUnderlyingCallsCount += 1 } + retrySendTargetReceivedArguments = (itemID: itemID, target: target) + retrySendTargetReceivedInvocationsLock.withLock { retrySendTargetUnderlyingReceivedInvocations.append((itemID: itemID, target: target)) } + await retrySendTargetClosure?(itemID, target) + } + //MARK: - abortSend + + private let abortSendTargetCallsCountLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingCallsCount = 0 + var abortSendTargetCallsCount: Int { + get { abortSendTargetCallsCountLock.withLock { abortSendTargetUnderlyingCallsCount } } + set { abortSendTargetCallsCountLock.withLock { abortSendTargetUnderlyingCallsCount = newValue } } + } + var abortSendTargetCalled: Bool { + return abortSendTargetCallsCount > 0 + } + private let abortSendTargetReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingReceivedArguments: (itemID: TimelineItemIdentifier, target: SendTarget)? + var abortSendTargetReceivedArguments: (itemID: TimelineItemIdentifier, target: SendTarget)? { + get { abortSendTargetReceivedArgumentsLock.withLock { abortSendTargetUnderlyingReceivedArguments } } + set { abortSendTargetReceivedArgumentsLock.withLock { abortSendTargetUnderlyingReceivedArguments = newValue } } + } + private let abortSendTargetReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingReceivedInvocations: [(itemID: TimelineItemIdentifier, target: SendTarget)] = [] + var abortSendTargetReceivedInvocations: [(itemID: TimelineItemIdentifier, target: SendTarget)] { + get { abortSendTargetReceivedInvocationsLock.withLock { abortSendTargetUnderlyingReceivedInvocations } } + set { abortSendTargetReceivedInvocationsLock.withLock { abortSendTargetUnderlyingReceivedInvocations = newValue } } + } + nonisolated(unsafe) var abortSendTargetClosure: ((TimelineItemIdentifier, SendTarget) async -> Void)? + + @concurrent func abortSend(_ itemID: TimelineItemIdentifier, target: SendTarget) async { + abortSendTargetCallsCountLock.withLock { abortSendTargetUnderlyingCallsCount += 1 } + abortSendTargetReceivedArguments = (itemID: itemID, target: target) + abortSendTargetReceivedInvocationsLock.withLock { abortSendTargetUnderlyingReceivedInvocations.append((itemID: itemID, target: target)) } + await abortSendTargetClosure?(itemID, target) + } //MARK: - eventTimestamp private let eventTimestampForCallsCountLock = NSLock() @@ -13292,6 +13396,90 @@ nonisolated class TimelineProxyMock: TimelineProxyProtocol, @unchecked Sendable return redactReasonReturnValue } } + //MARK: - retrySend + + private let retrySendTargetCallsCountLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingCallsCount = 0 + var retrySendTargetCallsCount: Int { + get { retrySendTargetCallsCountLock.withLock { retrySendTargetUnderlyingCallsCount } } + set { retrySendTargetCallsCountLock.withLock { retrySendTargetUnderlyingCallsCount = newValue } } + } + var retrySendTargetCalled: Bool { + return retrySendTargetCallsCount > 0 + } + private let retrySendTargetReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingReceivedArguments: (eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)? + var retrySendTargetReceivedArguments: (eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)? { + get { retrySendTargetReceivedArgumentsLock.withLock { retrySendTargetUnderlyingReceivedArguments } } + set { retrySendTargetReceivedArgumentsLock.withLock { retrySendTargetUnderlyingReceivedArguments = newValue } } + } + private let retrySendTargetReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingReceivedInvocations: [(eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)] = [] + var retrySendTargetReceivedInvocations: [(eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)] { + get { retrySendTargetReceivedInvocationsLock.withLock { retrySendTargetUnderlyingReceivedInvocations } } + set { retrySendTargetReceivedInvocationsLock.withLock { retrySendTargetUnderlyingReceivedInvocations = newValue } } + } + + private let retrySendTargetReturnValueLock = NSLock() + private nonisolated(unsafe) var retrySendTargetUnderlyingReturnValue: Result! + var retrySendTargetReturnValue: Result! { + get { retrySendTargetReturnValueLock.withLock { retrySendTargetUnderlyingReturnValue } } + set { retrySendTargetReturnValueLock.withLock { retrySendTargetUnderlyingReturnValue = newValue } } + } + nonisolated(unsafe) var retrySendTargetClosure: ((TimelineItemIdentifier.EventOrTransactionID, SendTarget) async -> Result)? + + @concurrent func retrySend(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget) async -> Result { + retrySendTargetCallsCountLock.withLock { retrySendTargetUnderlyingCallsCount += 1 } + retrySendTargetReceivedArguments = (eventOrTransactionID: eventOrTransactionID, target: target) + retrySendTargetReceivedInvocationsLock.withLock { retrySendTargetUnderlyingReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, target: target)) } + if let retrySendTargetClosure = retrySendTargetClosure { + return await retrySendTargetClosure(eventOrTransactionID, target) + } else { + return retrySendTargetReturnValue + } + } + //MARK: - abortSend + + private let abortSendTargetCallsCountLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingCallsCount = 0 + var abortSendTargetCallsCount: Int { + get { abortSendTargetCallsCountLock.withLock { abortSendTargetUnderlyingCallsCount } } + set { abortSendTargetCallsCountLock.withLock { abortSendTargetUnderlyingCallsCount = newValue } } + } + var abortSendTargetCalled: Bool { + return abortSendTargetCallsCount > 0 + } + private let abortSendTargetReceivedArgumentsLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingReceivedArguments: (eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)? + var abortSendTargetReceivedArguments: (eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)? { + get { abortSendTargetReceivedArgumentsLock.withLock { abortSendTargetUnderlyingReceivedArguments } } + set { abortSendTargetReceivedArgumentsLock.withLock { abortSendTargetUnderlyingReceivedArguments = newValue } } + } + private let abortSendTargetReceivedInvocationsLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingReceivedInvocations: [(eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)] = [] + var abortSendTargetReceivedInvocations: [(eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget)] { + get { abortSendTargetReceivedInvocationsLock.withLock { abortSendTargetUnderlyingReceivedInvocations } } + set { abortSendTargetReceivedInvocationsLock.withLock { abortSendTargetUnderlyingReceivedInvocations = newValue } } + } + + private let abortSendTargetReturnValueLock = NSLock() + private nonisolated(unsafe) var abortSendTargetUnderlyingReturnValue: Result! + var abortSendTargetReturnValue: Result! { + get { abortSendTargetReturnValueLock.withLock { abortSendTargetUnderlyingReturnValue } } + set { abortSendTargetReturnValueLock.withLock { abortSendTargetUnderlyingReturnValue = newValue } } + } + nonisolated(unsafe) var abortSendTargetClosure: ((TimelineItemIdentifier.EventOrTransactionID, SendTarget) async -> Result)? + + @concurrent func abortSend(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget) async -> Result { + abortSendTargetCallsCountLock.withLock { abortSendTargetUnderlyingCallsCount += 1 } + abortSendTargetReceivedArguments = (eventOrTransactionID: eventOrTransactionID, target: target) + abortSendTargetReceivedInvocationsLock.withLock { abortSendTargetUnderlyingReceivedInvocations.append((eventOrTransactionID: eventOrTransactionID, target: target)) } + if let abortSendTargetClosure = abortSendTargetClosure { + return await abortSendTargetClosure(eventOrTransactionID, target) + } else { + return abortSendTargetReturnValue + } + } //MARK: - pin private let pinEventIDCallsCountLock = NSLock() diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 4c0f24b44..240bfc7c1 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -264,7 +264,7 @@ struct TimelineRedactConfirmationInfo: Identifiable { enum TimelineAlertInfoType: Hashable { case audioRecodingPermissionError case pollEndConfirmation(String) - case sendingFailed(reason: String?, sendHandle: SendHandleProxy?) + case sendingFailed(reason: String?, itemID: TimelineItemIdentifier, target: SendTarget?) case encryptionAuthenticity(String) case encryptionForwarder(String) case inviteAgain diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 4ef6f9580..01c61790d 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -758,8 +758,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } if case let .sendingFailed(.unknown(reason)) = eventTimelineItem.properties.deliveryStatus { - // A missing send handle only costs the retry/remove actions, the reason is still worth showing. - displayAlert(.sendingFailed(reason: reason, sendHandle: timelineController.sendHandle(for: itemID))) + // A missing target only costs the retry/remove actions, the reason is still worth showing. + displayAlert(.sendingFailed(reason: reason, itemID: itemID, target: timelineController.pendingSendTarget(for: itemID))) } else if case let .sendingFailed(.verifiedUser(failure)) = eventTimelineItem.properties.deliveryStatus { guard let sendHandle = timelineController.sendHandle(for: itemID) else { MXLog.error("Cannot find send handle for \(itemID).") @@ -776,13 +776,12 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } } - private func retrySending(_ sendHandle: SendHandleProxy) { - Task { - if case .failure(let error) = await sendHandle.resend() { - MXLog.error("Failed retrying to send \(sendHandle.itemID): \(error)") - displayErrorToast(L10n.errorUnknown) - } - } + private func retrySending(_ itemID: TimelineItemIdentifier, target: SendTarget) { + Task { await timelineController.retrySend(itemID, target: target) } + } + + private func abortSending(_ itemID: TimelineItemIdentifier, target: SendTarget) { + Task { await timelineController.abortSend(itemID, target: target) } } private func slashCommand(message: String) -> SlashCommand? { @@ -1125,15 +1124,15 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { message: L10n.commonPollEndConfirmation, primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), secondaryButton: .init(title: L10n.actionOk) { self.timelineInteractionHandler.endPoll(pollStartID: pollStartID) }) - case .sendingFailed(let reason, let sendHandle): + case .sendingFailed(let reason, let itemID, let target): state.bindings.alertInfo = .init(id: type, title: L10n.commonSendingFailed, message: reason, - primaryButton: .init(title: sendHandle == nil ? L10n.actionOk : L10n.actionCancel, role: .cancel, action: nil), - verticalButtons: sendHandle.map { sendHandle in - [.init(title: L10n.actionRetry) { [weak self] in self?.retrySending(sendHandle) }, - .init(title: L10n.actionRemoveMessage, role: .destructive) { [weak self] in - self?.timelineInteractionHandler.redact(sendHandle.itemID, reason: nil) + primaryButton: .init(title: target == nil ? L10n.actionOk : L10n.actionCancel, role: .cancel, action: nil), + verticalButtons: target.map { target in + [.init(title: L10n.actionRetry) { [weak self] in self?.retrySending(itemID, target: target) }, + .init(title: target == .event ? L10n.actionRemoveMessage : L10n.actionDiscard, role: .destructive) { [weak self] in + self?.abortSending(itemID, target: target) }] }) case .encryptionAuthenticity(let message): diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 12ccdcf6e..ff304426f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -288,6 +288,53 @@ class TimelineController: TimelineControllerProtocol { return nil } + func pendingSendTarget(for itemID: TimelineItemIdentifier) -> SendTarget? { + for timelineItemProxy in activeTimelineItemProvider.itemProxies { + switch timelineItemProxy { + case .event(let item): + if item.id == itemID { + return item.pendingSend?.target + } + default: + continue + } + } + + return nil + } + + func retrySend(_ itemID: TimelineItemIdentifier, target: SendTarget) async { + guard let eventOrTransactionID = itemID.eventOrTransactionID else { + MXLog.error("Cannot retry sending on \(itemID).") + return + } + + MXLog.info("Retry \(target) send in \(roomID)") + + switch await activeTimeline.retrySend(eventOrTransactionID, target: target) { + case .success(let hadPendingSend): + MXLog.info("Finished retrying send, had one pending: \(hadPendingSend)") + case .failure(let error): + MXLog.error("Failed retrying send with error: \(error)") + } + } + + func abortSend(_ itemID: TimelineItemIdentifier, target: SendTarget) async { + guard let eventOrTransactionID = itemID.eventOrTransactionID else { + MXLog.error("Cannot abort sending on \(itemID).") + return + } + + MXLog.info("Abort \(target) send in \(roomID)") + + switch await activeTimeline.abortSend(eventOrTransactionID, target: target) { + case .success(let hadPendingSend): + MXLog.info("Finished aborting send, had one pending: \(hadPendingSend)") + case .failure(let error): + MXLog.error("Failed aborting send with error: \(error)") + } + } + // MARK: - Sending func sendMessage(_ message: String, diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index cc91239a2..2d176e3f4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -96,6 +96,12 @@ protocol TimelineControllerProtocol: Sendable { func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? + func pendingSendTarget(for itemID: TimelineItemIdentifier) -> SendTarget? + + func retrySend(_ itemID: TimelineItemIdentifier, target: SendTarget) async + + func abortSend(_ itemID: TimelineItemIdentifier, target: SendTarget) async + func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? // MARK: - Sending diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 93071f123..918d90822 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -84,11 +84,11 @@ final nonisolated class EventTimelineItemProxy: Sendable { } var deliveryStatus: TimelineItemDeliveryStatus? { - guard let localSendState = item.localSendState else { + guard let pendingSend else { return nil } - switch localSendState { + switch pendingSend.state { case .sendingFailed(let error, _): switch error { case .identityViolations(let users): @@ -107,6 +107,19 @@ final nonisolated class EventTimelineItemProxy: Sendable { } } + /// The item's own pending send or, when it has already gone out, our pending edit or redaction of it. + var pendingSend: (target: SendTarget, state: EventSendState)? { + if let state = item.localSendState { + (.event, state) + } else if let state = item.editSendState { + (.edit, state) + } else if let state = item.redactionSendState { + (.redaction, state) + } else { + nil + } + } + var canBeRepliedTo: Bool { item.canBeRepliedTo } diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 4062affab..9ad75fb20 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -213,6 +213,36 @@ final class TimelineProxy: TimelineProxyProtocol { } } + func retrySend(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget) async -> Result { + MXLog.info("Retrying \(target) send on timeline item: \(eventOrTransactionID)") + + do { + let hadPendingSend = try await timeline.retrySend(itemId: eventOrTransactionID.rustValue, target: target) + + MXLog.info("Retried \(target) send on timeline item: \(eventOrTransactionID), had one pending: \(hadPendingSend)") + + return .success(hadPendingSend) + } catch { + MXLog.error("Failed retrying \(target) send on timeline item: \(eventOrTransactionID) with error: \(error)") + return .failure(.sdkError(error)) + } + } + + func abortSend(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, target: SendTarget) async -> Result { + MXLog.info("Aborting \(target) send on timeline item: \(eventOrTransactionID)") + + do { + let hadPendingSend = try await timeline.abortSend(itemId: eventOrTransactionID.rustValue, target: target) + + MXLog.info("Aborted \(target) send on timeline item: \(eventOrTransactionID), had one pending: \(hadPendingSend)") + + return .success(hadPendingSend) + } catch { + MXLog.error("Failed aborting \(target) send on timeline item: \(eventOrTransactionID) with error: \(error)") + return .failure(.sdkError(error)) + } + } + func getLoadedReplyDetails(eventID: String) async -> Result { do { return try await .success(timeline.loadReplyDetails(eventIdStr: eventID)) diff --git a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift index 0f406969c..33ee54ec4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxyProtocol.swift @@ -99,6 +99,12 @@ protocol TimelineProxyProtocol: Sendable { func redact(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, reason: String?) async -> Result + func retrySend(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, + target: SendTarget) async -> Result + + func abortSend(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, + target: SendTarget) async -> Result + func pin(eventID: String) async -> Result func unpin(eventID: String) async -> Result diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index 57f135402..d5977b8f2 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -549,7 +549,7 @@ final class TimelineViewModelTests { // Given a room with a message that failed to send for a known reason let items = [TextRoomTimelineItem(eventID: "t1", sendFailure: .unknown(reason: "M_TOO_LARGE"))] let timelineController = TimelineControllerMock(.init(timelineItems: items)) - timelineController.sendHandleForReturnValue = .mock + timelineController.pendingSendTargetForReturnValue = .event let viewModel = makeViewModel(timelineController: timelineController) viewModel.process(viewAction: .itemSendInfoTapped(itemID: items[0].id))