diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index ca85a6cbb..7d327029b 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -222,7 +222,7 @@ struct ReadReceiptSummaryInfo: Identifiable { enum TimelineAlertInfoType: Hashable { case audioRecodingPermissionError case pollEndConfirmation(String) - case sendingFailed + case sendingFailed(reason: String?, itemID: TimelineItemIdentifier) 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 76e5a83aa..f9528d0db 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -738,8 +738,8 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { fatalError("Only events can have send info.") } - if case .sendingFailed(.unknown) = eventTimelineItem.properties.deliveryStatus { - displayAlert(.sendingFailed) + if case let .sendingFailed(.unknown(reason)) = eventTimelineItem.properties.deliveryStatus { + displayAlert(.sendingFailed(reason: reason, itemID: 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).") @@ -755,6 +755,27 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { displayAlert(.encryptionAuthenticity(authenticityMessage)) } } + + private func retrySending(itemID: TimelineItemIdentifier) { + guard let sendHandle = timelineController.sendHandle(for: itemID) else { + MXLog.error("Cannot find send handle for \(itemID).") + return + } + + Task { + if case .failure(let error) = await sendHandle.resend() { + MXLog.error("Failed retrying to send \(itemID): \(error)") + } + } + } + + private func removeFailedMessage(itemID: TimelineItemIdentifier) { + guard case let .event(_, eventOrTransactionID) = itemID else { + fatalError("Only events can fail to send.") + } + + Task { await timelineController.redact(eventOrTransactionID) } + } private func slashCommand(message: String) -> SlashCommand? { for command in SlashCommand.allCases where message.starts(with: command.rawValue) { @@ -1103,10 +1124,13 @@ 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: + case .sendingFailed(let reason, let itemID): state.bindings.alertInfo = .init(id: type, title: L10n.commonSendingFailed, - primaryButton: .init(title: L10n.actionOk, action: nil)) + message: reason, + primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil), + verticalButtons: [.init(title: L10n.actionRetry) { [weak self] in self?.retrySending(itemID: itemID) }, + .init(title: L10n.actionRemoveMessage, role: .destructive) { [weak self] in self?.removeFailedMessage(itemID: itemID) }]) case .encryptionAuthenticity(let message): state.bindings.alertInfo = .init(id: type, title: message, diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index d16cb49b1..d9a418832 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -262,7 +262,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { static let (backupItem, _) = makeActions(authenticity: .notGuaranteed(color: .gray)) static let (unsignedItem, _) = makeActions(authenticity: .unsignedDevice(color: .red)) static let (unencryptedItem, _) = makeActions(authenticity: .sentInClear(color: .red)) - static let (unknownFailureItem, _) = makeActions(deliveryStatus: .sendingFailed(.unknown)) + static let (unknownFailureItem, _) = makeActions(deliveryStatus: .sendingFailed(.unknown(reason: nil))) static let (identityChangedItem, _) = makeActions(deliveryStatus: .sendingFailed(.verifiedUser(.changedIdentity(users: [ "@alice:matrix.org" ])))) diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index e2d16acee..d1feef6c7 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -92,7 +92,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { static let failed: TextRoomTimelineItem = { var result = base - result.properties.deliveryStatus = .sendingFailed(.unknown) + result.properties.deliveryStatus = .sendingFailed(.unknown(reason: nil)) return result }() diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index c897ecd22..69a744b44 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -65,7 +65,7 @@ nonisolated enum TimelineItemSendFailure: Hashable { } case verifiedUser(VerifiedUser) - case unknown + case unknown(reason: String?) } /// A light wrapper around event timeline items returned from Rust. @@ -95,8 +95,10 @@ final nonisolated class EventTimelineItemProxy: Sendable { return .sendingFailed(.verifiedUser(.changedIdentity(users: users))) case .insecureDevices(let userDeviceMap): return .sendingFailed(.verifiedUser(.hasUnsignedDevice(devices: userDeviceMap))) + case .genericApiError(let message): + return .sendingFailed(.unknown(reason: message)) default: - return .sendingFailed(.unknown) + return .sendingFailed(.unknown(reason: nil)) } case .notSentYet: return .sending