diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index 7d327029b..19abe2233 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(reason: String?, itemID: TimelineItemIdentifier) + case sendingFailed(reason: String?, sendHandle: SendHandleProxy) 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 f9528d0db..898068470 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -739,7 +739,12 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } if case let .sendingFailed(.unknown(reason)) = eventTimelineItem.properties.deliveryStatus { - displayAlert(.sendingFailed(reason: reason, itemID: itemID)) + guard let sendHandle = timelineController.sendHandle(for: itemID) else { + MXLog.error("Cannot find send handle for \(itemID).") + return + } + + displayAlert(.sendingFailed(reason: reason, sendHandle: sendHandle)) } 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,27 +760,15 @@ 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 - } - + + private func retrySending(_ sendHandle: SendHandleProxy) { Task { if case .failure(let error) = await sendHandle.resend() { - MXLog.error("Failed retrying to send \(itemID): \(error)") + MXLog.error("Failed retrying to send \(sendHandle.itemID): \(error)") + userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) } } } - - 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) { @@ -1124,13 +1117,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 itemID): + case .sendingFailed(let reason, let sendHandle): state.bindings.alertInfo = .init(id: type, title: L10n.commonSendingFailed, 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) }]) + verticalButtons: [.init(title: L10n.actionRetry) { [weak self] in self?.retrySending(sendHandle) }, + .init(title: L10n.actionRemoveMessage, role: .destructive) { [weak self] in + self?.timelineInteractionHandler.handleTimelineItemMenuAction(.redact(isMedia: false), itemID: sendHandle.itemID) + }]) case .encryptionAuthenticity(let message): state.bindings.alertInfo = .init(id: type, title: message, diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift index 1a2ea27a7..ef3b53d0b 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemSendInfoLabel.swift @@ -48,7 +48,9 @@ private struct TimelineItemSendInfoModifier: ViewModifier { TimelineItemSendInfoLabel(sendInfo: sendInfo) .accessibilityHidden(isAccessibilityHidden) .contentShape(.rect) - .accessibilityIdentifier(A11yIdentifiers.roomScreen.sendInfo) + // Only the labels with a tappable status get the identifier, so that + // plain timestamps don't produce ambiguous matches in UI tests. + .accessibilityIdentifier(sendInfo.status != nil ? A11yIdentifiers.roomScreen.sendInfo : "") // Tap gesture to avoid the message being detected as a button by VoiceOver // (and the action shows a description that is already read to the user). .onTapGesture {