Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 47 additions & 38 deletions Meshtastic/Enums/RoutingError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,83 +27,88 @@ enum RoutingError: Int, CaseIterable, Identifiable {
case adminBadSessionKey = 36
case adminPublicKeyUnauthorized = 37
case rateLimitExceeded = 38
case pkiSendFailPublicKey = 39

var id: Int { self.rawValue }
var display: String {
switch self {

case .none:
return "Acknowledged".localized
return "Delivered to recipient".localized
case .noRoute:
return "No Route".localized
return "No route to recipient".localized
case .gotNak:
return "Received a negative acknowledgment".localized
return "Message was rejected".localized
case .timeout:
return "Timeout".localized
return "Timed out".localized
case .noInterface:
return "No Interface".localized
return "Radio interface unavailable".localized
case .maxRetransmit:
return "Max Retransmission Reached".localized
return "Failed to deliver to mesh".localized
case .noChannel:
return "No Channel".localized
return "No channel selected".localized
case .tooLarge:
return "The packet is too large".localized
return "Message is too large to send".localized
case .noResponse:
return "No Response".localized
return "No response from recipient".localized
case .dutyCycleLimit:
return "Regional Duty Cycle Limit Reached".localized
return "Duty cycle limit reached".localized
case .badRequest:
return "Bad Request".localized
return "Message request invalid".localized
case .notAuthorized:
return "Not Authorized".localized
return "Not authorized to send".localized
case .pkiFailed:
return "Encrypted Send Failed".localized
return "Could not send encrypted message".localized
case .pkiUnknownPubkey:
return "Unknown public key".localized
return "Recipient needs your key".localized
case .adminBadSessionKey:
return "Bad admin session key".localized
return "Admin session expired".localized
case .adminPublicKeyUnauthorized:
return "Unauthorized admin public key".localized
return "Admin key not authorized".localized
case .rateLimitExceeded:
return "Rate Limit Exceeded".localized
return "Sending too quickly".localized
case .pkiSendFailPublicKey:
return "Recipient key unavailable".localized
}
}
var description: String {
switch self {
case .none:
return "Message was successfully delivered to the recipient.".localized
return "The recipient confirmed this message.".localized
case .noRoute:
return "No route to the destination node was found in the mesh.".localized
return "No route to the destination node was found in the mesh. Try again when more nodes are reachable.".localized
case .gotNak:
return "A node in the path explicitly rejected the packet.".localized
return "A node in the path rejected this message. Try again when the route changes.".localized
case .timeout:
return "No acknowledgment was received within the expected time window.".localized
return "No acknowledgment was received in time. Try again when you have better signal or more mesh coverage.".localized
case .noInterface:
return "The radio interface needed to send the packet is unavailable.".localized
return "The radio interface needed to send this message is unavailable.".localized
case .maxRetransmit:
return "The packet was retransmitted the maximum number of times without acknowledgment.".localized
return "No node confirmed this message. Try again when you have better signal or more mesh coverage.".localized
case .noChannel:
return "The channel required for this message is not configured on the device.".localized
return "Select a channel before sending.".localized
case .tooLarge:
return "The message exceeds the maximum packet size and cannot be sent.".localized
return "Shorten the message and send it again.".localized
case .noResponse:
return "The destination node did not respond to the request.".localized
return "The recipient did not respond. Try again when the recipient is reachable.".localized
case .dutyCycleLimit:
return "The regional duty cycle limit has been reached; transmissions are temporarily paused.".localized
return "Your region's duty cycle limit has been reached. Wait before sending again.".localized
case .badRequest:
return "The request was malformed or contained invalid parameters.".localized
return "The radio could not send this message because the request was invalid.".localized
case .notAuthorized:
return "The requesting node is not authorized to perform this action.".localized
return "Your node is not authorized to send this message in the current context.".localized
case .pkiFailed:
return "Public key encryption failed; the message could not be encrypted for the recipient.".localized
return "Encryption failed. Wait for node info or keys to sync, then try again.".localized
case .pkiUnknownPubkey:
return "The recipient's public key is not known; direct message encryption is not possible.".localized
return "The recipient does not know your public key yet. Your node may share its info automatically; try again after it syncs.".localized
case .adminBadSessionKey:
return "The admin session key is invalid or has expired.".localized
return "The admin session key is invalid or expired. Request a new session before trying again.".localized
case .adminPublicKeyUnauthorized:
return "The admin public key is not in the authorized list on the remote node.".localized
return "The remote node does not authorize your admin public key.".localized
case .rateLimitExceeded:
return "Too many requests were sent in a short period; wait before retrying.".localized
return "Too many messages were sent in a short period. Wait before trying again.".localized
case .pkiSendFailPublicKey:
return "Your node does not have the recipient's public key yet. Wait for node info to sync, then try again.".localized
}
}
var color: Color {
Expand All @@ -130,27 +135,29 @@ enum RoutingError: Int, CaseIterable, Identifiable {
case .maxRetransmit:
return true
case .noChannel:
return true
return false
case .tooLarge:
return false
case .noResponse:
return true
case .dutyCycleLimit:
return true
case .badRequest:
return true
return false
case .notAuthorized:
return true
return false
case .pkiFailed:
return true
case .pkiUnknownPubkey:
return true
case .adminBadSessionKey:
return true
case .adminPublicKeyUnauthorized:
return true
return false
case .rateLimitExceeded:
return true
case .pkiSendFailPublicKey:
return true
}
}
func protoEnumValue() -> Routing.Error {
Expand Down Expand Up @@ -191,6 +198,8 @@ enum RoutingError: Int, CaseIterable, Identifiable {
return Routing.Error.adminPublicKeyUnauthorized
case .rateLimitExceeded:
return Routing.Error.rateLimitExceeded
case .pkiSendFailPublicKey:
return Routing.Error.pkiSendFailPublicKey
}
}
}
91 changes: 91 additions & 0 deletions Meshtastic/Extensions/SwiftData/MessageEntityExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,74 @@ import Foundation
import MapKit
import SwiftUI

struct MessageDeliveryStatus {
let text: String
let detail: String
let systemImage: String
let color: Color
let canRetry: Bool

static let sending = MessageDeliveryStatus(
text: "Sending...".localized,
detail: "Waiting for the mesh to acknowledge this message.".localized,
systemImage: "clock",
color: Color(uiColor: .systemOrange),
canRetry: false
)

static let deliveredToMesh = MessageDeliveryStatus(
text: "Delivered to mesh".localized,
detail: "A node on the mesh confirmed this message.".localized,
systemImage: "checkmark.circle.fill",
color: Color(uiColor: .secondaryLabel),
canRetry: false
)

static let relayedNotConfirmed = MessageDeliveryStatus(
text: "Relayed, not confirmed by recipient".localized,
detail: "A node relayed this message, but the recipient has not confirmed it.".localized,
systemImage: "exclamationmark.circle.fill",
color: Color(uiColor: .systemOrange),
canRetry: true
)

static let deliveredToRecipient = MessageDeliveryStatus(
text: "Delivered to recipient".localized,
detail: "The recipient confirmed this message.".localized,
systemImage: "checkmark.circle.fill",
color: Color(uiColor: .secondaryLabel),
canRetry: false
)

static func failed(_ error: RoutingError) -> MessageDeliveryStatus {
MessageDeliveryStatus(
text: error.display,
detail: error.description,
systemImage: error.canRetry ? "exclamationmark.circle.fill" : "xmark.circle.fill",
color: error.color,
canRetry: error.canRetry
)
}
}

struct MessageDeliveryStatusLabel: View {
let status: MessageDeliveryStatus

var body: some View {
Label {
Text(status.text)
.fixedSize(horizontal: false, vertical: true)
} icon: {
Image(systemName: status.systemImage)
.imageScale(.small)
}
.font(.caption2)
.foregroundStyle(status.color)
.labelStyle(.titleAndIcon)
.accessibilityLabel(status.text)
}
}

extension MessageEntity {
var hasTranslatedPayload: Bool {
!(messagePayloadTranslated?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
Expand Down Expand Up @@ -40,6 +108,29 @@ extension MessageEntity {
return re?.canRetry ?? false
}

func deliveryStatus(isDirectMessage: Bool) -> MessageDeliveryStatus {
if receivedACK {
if isDirectMessage {
return realACK ? .deliveredToRecipient : .relayedNotConfirmed
}
return .deliveredToMesh
}

guard ackError != 0 else { return .sending }

if let routingError = RoutingError(rawValue: Int(ackError)) {
return .failed(routingError)
}

return MessageDeliveryStatus(
text: "Could not send message".localized,
detail: "The radio reported an unknown delivery error.".localized,
systemImage: "exclamationmark.circle.fill",
color: Color(uiColor: .systemOrange),
canRetry: true
)
}

@MainActor
var tapbacks: [MessageEntity] {
let context = PersistenceController.shared.context
Expand Down
50 changes: 40 additions & 10 deletions Meshtastic/Views/Helpers/Help/AckErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,50 @@ struct AckErrors: View {
Section {
HelpItem(
symbol: AnyView(
RoundedRectangle(cornerRadius: 5)
.fill(.orange)
.frame(width: 20, height: 12)
Image(systemName: MessageDeliveryStatus.sending.systemImage)
.foregroundStyle(MessageDeliveryStatus.sending.color)
.frame(width: 20)
),
title: String(localized: "Acknowledged by another node"),
subtitle: String(localized: "Message was relayed but not confirmed by the final recipient."),
title: MessageDeliveryStatus.sending.text,
subtitle: MessageDeliveryStatus.sending.detail,
compact: true
)
ForEach(RoutingError.allCases) { re in
HelpItem(
symbol: AnyView(
Image(systemName: MessageDeliveryStatus.deliveredToMesh.systemImage)
.foregroundStyle(MessageDeliveryStatus.deliveredToMesh.color)
.frame(width: 20)
),
title: MessageDeliveryStatus.deliveredToMesh.text,
subtitle: MessageDeliveryStatus.deliveredToMesh.detail,
compact: true
)
HelpItem(
symbol: AnyView(
Image(systemName: MessageDeliveryStatus.relayedNotConfirmed.systemImage)
.foregroundStyle(MessageDeliveryStatus.relayedNotConfirmed.color)
.frame(width: 20)
),
title: MessageDeliveryStatus.relayedNotConfirmed.text,
subtitle: MessageDeliveryStatus.relayedNotConfirmed.detail,
compact: true
)
HelpItem(
symbol: AnyView(
Image(systemName: MessageDeliveryStatus.deliveredToRecipient.systemImage)
.foregroundStyle(MessageDeliveryStatus.deliveredToRecipient.color)
.frame(width: 20)
),
title: MessageDeliveryStatus.deliveredToRecipient.text,
subtitle: MessageDeliveryStatus.deliveredToRecipient.detail,
compact: true
)
ForEach(RoutingError.allCases.filter { $0 != .none }) { re in
HelpItem(
symbol: AnyView(
RoundedRectangle(cornerRadius: 5)
.fill(re.color)
.frame(width: 20, height: 12)
Image(systemName: re.canRetry ? "exclamationmark.circle.fill" : "xmark.circle.fill")
.foregroundStyle(re.color)
.frame(width: 20)
),
title: re.display,
subtitle: re.description,
Expand All @@ -36,7 +66,7 @@ struct AckErrors: View {
} header: {
Text("Message Status")
} footer: {
Text("Grey indicates successful delivery. Orange indicates a retryable error. Red indicates a permanent failure that will not succeed on retry.")
Text("Text is shown with icon and color. Gray indicates delivery, orange indicates sending or a retryable warning, and red indicates a permanent failure that will not succeed on retry.")
}
}
}
Expand Down
13 changes: 3 additions & 10 deletions Meshtastic/Views/Messages/ChannelMessageRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ struct ChannelMessageRow: View {
onTapback(message)
}

if isCurrentUser && message.canRetry {
if isCurrentUser && message.deliveryStatus(isDirectMessage: false).canRetry {
RetryButton(message: message, destination: .channel(channel))
}
}
Expand All @@ -149,15 +149,8 @@ struct ChannelMessageRow: View {

// ACK Status / Error
HStack {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
if isCurrentUser && message.receivedACK {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red).font(.caption2)
} else if isCurrentUser && message.ackError == 0 {
Text("Waiting to be acknowledged. . .").font(.caption2).foregroundColor(.orange)
} else if isCurrentUser && !isDetectionSensorMessage {
Text("\(ackErrorVal?.display ?? "Empty Ack Error")").fixedSize(horizontal: false, vertical: true)
.foregroundStyle(ackErrorVal?.color ?? Color.red).font(.caption2)
if isCurrentUser && !isDetectionSensorMessage {
MessageDeliveryStatusLabel(status: message.deliveryStatus(isDirectMessage: false))
}
}
}
Expand Down
Loading
Loading