Skip to content

Commit e886046

Browse files
authored
Align message delivery status wording (#2054)
Aligns outgoing message delivery status labels with the cross-platform design language (meshtastic/design#43). Closes #2051.
1 parent dd19e79 commit e886046

19 files changed

Lines changed: 571 additions & 239 deletions

File tree

Meshtastic.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
6DA39D8E2A92DC52007E311C /* MeshtasticAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */; };
145145
6DEDA55A2A957B8E00321D2E /* DetectionSensorLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */; };
146146
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */; };
147+
AA2054D12F9E000100000001 /* MessageDeliveryStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA2054D02F9E000100000001 /* MessageDeliveryStatus.swift */; };
147148
70A5B362F7EC4F14A99A8C39 /* UserEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 918722D2C1474B2D99ED01DC /* UserEntity.swift */; };
148149
74BEA4429B3945AEBA11E977 /* MeshtasticMigrationPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CAEAD87807B4F34820A60C2 /* MeshtasticMigrationPlan.swift */; };
149150
7717E5954788B23527BACF65 /* IntentMessageConverters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D80697D5BB72C4566988E0 /* IntentMessageConverters.swift */; };
@@ -756,6 +757,7 @@
756757
6DA39D8D2A92DC52007E311C /* MeshtasticAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeshtasticAppDelegate.swift; sourceTree = "<group>"; };
757758
6DEDA5592A957B8E00321D2E /* DetectionSensorLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetectionSensorLog.swift; sourceTree = "<group>"; };
758759
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageEntityExtension.swift; sourceTree = "<group>"; };
760+
AA2054D02F9E000100000001 /* MessageDeliveryStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDeliveryStatus.swift; sourceTree = "<group>"; };
759761
7101240749F24E96835A040A /* NodeInfoEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeInfoEntity.swift; sourceTree = "<group>"; };
760762
748E4806582595DE80D455CD /* CoTXMLParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CoTXMLParser.swift; sourceTree = "<group>"; };
761763
7F1B62B5CB54395476C3A924 /* GenericCoTHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GenericCoTHandler.swift; sourceTree = "<group>"; };
@@ -1909,6 +1911,7 @@
19091911
children = (
19101912
108FFECC2DD4005600BFAA81 /* NodeInfoEntityToNodeInfo.swift */,
19111913
DD58C5F12919AD3C00D5BEFB /* ChannelEntityExtension.swift */,
1914+
AA2054D02F9E000100000001 /* MessageDeliveryStatus.swift */,
19121915
6DEDA55B2A9592F900321D2E /* MessageEntityExtension.swift */,
19131916
DD007BAD2AA4E91200F5FA12 /* MyInfoEntityExtension.swift */,
19141917
DD007BAF2AA5981000F5FA12 /* NodeInfoEntityExtension.swift */,
@@ -3119,6 +3122,7 @@
31193122
DD2160AF28C5552500C17253 /* MQTTConfig.swift in Sources */,
31203123
DD6F65722C6AB8EC0053C113 /* SecureInput.swift in Sources */,
31213124
DD13AA492AB73BF400BA0C98 /* PositionPopover.swift in Sources */,
3125+
AA2054D12F9E000100000001 /* MessageDeliveryStatus.swift in Sources */,
31223126
6DEDA55C2A9592F900321D2E /* MessageEntityExtension.swift in Sources */,
31233127
DD9C700D2E8D5A9500106227 /* ChannelMessageRow.swift in Sources */,
31243128
2315D1A02EECB44800E0FAE7 /* UF2MassStorageView.swift in Sources */,

Meshtastic/Enums/RoutingError.swift

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,83 +27,88 @@ enum RoutingError: Int, CaseIterable, Identifiable {
2727
case adminBadSessionKey = 36
2828
case adminPublicKeyUnauthorized = 37
2929
case rateLimitExceeded = 38
30+
case pkiSendFailPublicKey = 39
3031

3132
var id: Int { self.rawValue }
3233
var display: String {
3334
switch self {
3435

3536
case .none:
36-
return "Acknowledged".localized
37+
return "Delivered to recipient".localized
3738
case .noRoute:
38-
return "No Route".localized
39+
return "Failed to deliver to mesh".localized
3940
case .gotNak:
40-
return "Received a negative acknowledgment".localized
41+
return "Failed to deliver to mesh".localized
4142
case .timeout:
42-
return "Timeout".localized
43+
return "Failed to deliver to mesh".localized
4344
case .noInterface:
44-
return "No Interface".localized
45+
return "No radio interface".localized
4546
case .maxRetransmit:
46-
return "Max Retransmission Reached".localized
47+
return "Failed to deliver to mesh".localized
4748
case .noChannel:
48-
return "No Channel".localized
49+
return "Channel/key mismatch".localized
4950
case .tooLarge:
50-
return "The packet is too large".localized
51+
return "Message is too large to send".localized
5152
case .noResponse:
52-
return "No Response".localized
53+
return "No app response".localized
5354
case .dutyCycleLimit:
54-
return "Regional Duty Cycle Limit Reached".localized
55+
return "Duty cycle limit".localized
5556
case .badRequest:
56-
return "Bad Request".localized
57+
return "Invalid request".localized
5758
case .notAuthorized:
58-
return "Not Authorized".localized
59+
return "Not authorized".localized
5960
case .pkiFailed:
60-
return "Encrypted Send Failed".localized
61+
return "Could not send encrypted message".localized
6162
case .pkiUnknownPubkey:
62-
return "Unknown public key".localized
63+
return "Recipient needs your key".localized
6364
case .adminBadSessionKey:
64-
return "Bad admin session key".localized
65+
return "Admin session expired".localized
6566
case .adminPublicKeyUnauthorized:
66-
return "Unauthorized admin public key".localized
67+
return "Admin key not authorized".localized
6768
case .rateLimitExceeded:
68-
return "Rate Limit Exceeded".localized
69+
return "Rate limited".localized
70+
case .pkiSendFailPublicKey:
71+
return "Recipient key unavailable".localized
6972
}
7073
}
7174
var description: String {
7275
switch self {
7376
case .none:
74-
return "Message was successfully delivered to the recipient.".localized
77+
return "The recipient confirmed this message.".localized
7578
case .noRoute:
76-
return "No route to the destination node was found in the mesh.".localized
79+
return "No route to the destination node was found in the mesh. Try again when more nodes are reachable.".localized
7780
case .gotNak:
78-
return "A node in the path explicitly rejected the packet.".localized
81+
return "A node rejected this message. Try again when the route changes.".localized
7982
case .timeout:
80-
return "No acknowledgment was received within the expected time window.".localized
83+
return "No acknowledgment was received in time. Try again when you have better signal or more mesh coverage.".localized
8184
case .noInterface:
82-
return "The radio interface needed to send the packet is unavailable.".localized
85+
return "The sender has no usable radio interface for this message.".localized
8386
case .maxRetransmit:
84-
return "The packet was retransmitted the maximum number of times without acknowledgment.".localized
87+
return "No node confirmed this message. Try again when you have better signal or more mesh coverage.".localized
8588
case .noChannel:
86-
return "The channel required for this message is not configured on the device.".localized
89+
return "The sender or recipient could not use a matching channel/key for this message.".localized
8790
case .tooLarge:
88-
return "The message exceeds the maximum packet size and cannot be sent.".localized
91+
return "Shorten the message and send it again.".localized
8992
case .noResponse:
90-
return "The destination node did not respond to the request.".localized
93+
return "The destination received the request, but no app or module responded. Try again when the recipient is reachable.".localized
9194
case .dutyCycleLimit:
92-
return "The regional duty cycle limit has been reached; transmissions are temporarily paused.".localized
95+
return "Local airtime limits are temporarily blocking sends. Wait before trying again.".localized
9396
case .badRequest:
94-
return "The request was malformed or contained invalid parameters.".localized
97+
return "The destination rejected this request as invalid.".localized
9598
case .notAuthorized:
96-
return "The requesting node is not authorized to perform this action.".localized
99+
return "The destination refused this request because it is not authorized.".localized
97100
case .pkiFailed:
98-
return "Public key encryption failed; the message could not be encrypted for the recipient.".localized
101+
return "The encrypted send path could not be used. Wait for node info or keys to sync, then try again.".localized
99102
case .pkiUnknownPubkey:
100-
return "The recipient's public key is not known; direct message encryption is not possible.".localized
103+
return "The recipient does not know your public key yet. Your node may share its info automatically; try again after it syncs.".localized
101104
case .adminBadSessionKey:
102-
return "The admin session key is invalid or has expired.".localized
105+
return "The admin session key is missing, expired, or invalid. Request a new session before trying again.".localized
103106
case .adminPublicKeyUnauthorized:
104-
return "The admin public key is not in the authorized list on the remote node.".localized
107+
return "The remote node does not authorize your admin key.".localized
105108
case .rateLimitExceeded:
106-
return "Too many requests were sent in a short period; wait before retrying.".localized
109+
return "Messages are being sent too quickly. Wait before trying again.".localized
110+
case .pkiSendFailPublicKey:
111+
return "Your node does not have the recipient's public key yet. Wait for node info to sync, then try again.".localized
107112
}
108113
}
109114
var color: Color {
@@ -130,27 +135,29 @@ enum RoutingError: Int, CaseIterable, Identifiable {
130135
case .maxRetransmit:
131136
return true
132137
case .noChannel:
133-
return true
138+
return false
134139
case .tooLarge:
135140
return false
136141
case .noResponse:
137142
return true
138143
case .dutyCycleLimit:
139144
return true
140145
case .badRequest:
141-
return true
146+
return false
142147
case .notAuthorized:
143-
return true
148+
return false
144149
case .pkiFailed:
145150
return true
146151
case .pkiUnknownPubkey:
147152
return true
148153
case .adminBadSessionKey:
149154
return true
150155
case .adminPublicKeyUnauthorized:
151-
return true
156+
return false
152157
case .rateLimitExceeded:
153158
return true
159+
case .pkiSendFailPublicKey:
160+
return true
154161
}
155162
}
156163
func protoEnumValue() -> Routing.Error {
@@ -191,6 +198,8 @@ enum RoutingError: Int, CaseIterable, Identifiable {
191198
return Routing.Error.adminPublicKeyUnauthorized
192199
case .rateLimitExceeded:
193200
return Routing.Error.rateLimitExceeded
201+
case .pkiSendFailPublicKey:
202+
return Routing.Error.pkiSendFailPublicKey
194203
}
195204
}
196205
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import SwiftUI
2+
3+
struct MessageDeliveryStatus {
4+
let text: String
5+
let detail: String
6+
let systemImage: String
7+
let color: Color
8+
let canRetry: Bool
9+
10+
static let sending = MessageDeliveryStatus(
11+
text: "Sending...".localized,
12+
detail: "Waiting for the mesh to acknowledge this message.".localized,
13+
systemImage: "clock",
14+
color: Color(uiColor: .systemOrange),
15+
canRetry: false
16+
)
17+
18+
static let deliveredToMesh = MessageDeliveryStatus(
19+
text: "Delivered to mesh".localized,
20+
detail: "A node on the mesh confirmed this message.".localized,
21+
systemImage: "checkmark.circle.fill",
22+
color: Color(uiColor: .secondaryLabel),
23+
canRetry: false
24+
)
25+
26+
static let relayedNotConfirmed = MessageDeliveryStatus(
27+
text: "Relayed, not confirmed by recipient".localized,
28+
detail: "A node relayed this message, but the recipient has not confirmed it.".localized,
29+
systemImage: "exclamationmark.circle.fill",
30+
color: Color(uiColor: .systemOrange),
31+
canRetry: true
32+
)
33+
34+
static let deliveredToRecipient = MessageDeliveryStatus(
35+
text: "Delivered to recipient".localized,
36+
detail: "The recipient confirmed this message.".localized,
37+
systemImage: "checkmark.circle.fill",
38+
color: Color(uiColor: .secondaryLabel),
39+
canRetry: false
40+
)
41+
42+
static func failed(_ error: RoutingError) -> MessageDeliveryStatus {
43+
MessageDeliveryStatus(
44+
text: error.display,
45+
detail: error.description,
46+
systemImage: error.canRetry ? "exclamationmark.circle.fill" : "xmark.circle.fill",
47+
color: error.color,
48+
canRetry: error.canRetry
49+
)
50+
}
51+
}
52+
53+
struct MessageDeliveryStatusLabel: View {
54+
let status: MessageDeliveryStatus
55+
56+
var body: some View {
57+
Label {
58+
Text(status.text)
59+
.fixedSize(horizontal: false, vertical: true)
60+
} icon: {
61+
Image(systemName: status.systemImage)
62+
.imageScale(.small)
63+
}
64+
.font(.caption2)
65+
.foregroundStyle(status.color)
66+
.labelStyle(.titleAndIcon)
67+
.accessibilityLabel(status.text)
68+
}
69+
}

Meshtastic/Extensions/SwiftData/MessageEntityExtension.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,29 @@ extension MessageEntity {
4040
return re?.canRetry ?? false
4141
}
4242

43+
func deliveryStatus(isDirectMessage: Bool) -> MessageDeliveryStatus {
44+
if receivedACK {
45+
if isDirectMessage {
46+
return realACK ? .deliveredToRecipient : .relayedNotConfirmed
47+
}
48+
return .deliveredToMesh
49+
}
50+
51+
guard ackError != 0 else { return .sending }
52+
53+
if let routingError = RoutingError(rawValue: Int(ackError)) {
54+
return .failed(routingError)
55+
}
56+
57+
return MessageDeliveryStatus(
58+
text: "Could not send message".localized,
59+
detail: "The radio reported an unknown delivery error.".localized,
60+
systemImage: "exclamationmark.circle.fill",
61+
color: Color(uiColor: .systemOrange),
62+
canRetry: true
63+
)
64+
}
65+
4366
@MainActor
4467
var tapbacks: [MessageEntity] {
4568
let context = PersistenceController.shared.context
96.1 KB
Loading

Meshtastic/Resources/docs/index.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -278,38 +278,38 @@
278278
"section": "user",
279279
"navOrder": 3,
280280
"keywords": [
281-
"channel",
282281
"message",
283-
"channels",
284-
"messages",
282+
"channel",
285283
"key",
284+
"not",
285+
"circle",
286+
"messages",
287+
"fill",
288+
"channels",
286289
"text",
287290
"formatting",
288-
"not",
291+
"available",
292+
"orange",
289293
"markdown",
290294
"delimiters",
291-
"selection",
295+
"retry",
296+
"exclamationmark",
292297
"direct",
298+
"recipient",
299+
"node",
300+
"after",
301+
"selection",
293302
"bold",
294303
"tap",
295-
"from",
296-
"button",
297304
"public",
305+
"button",
306+
"symbol",
307+
"radio",
298308
"preview",
299309
"press",
300-
"node",
301-
"encryption",
302-
"destination",
303-
"broadcast",
304-
"verified",
305-
"toolbar",
306-
"tip",
307-
"symbol",
308-
"style",
309-
"shield",
310-
"select"
310+
"from"
311311
],
312-
"charCount": 9014
312+
"charCount": 10415
313313
},
314314
{
315315
"id": "mqtt",

0 commit comments

Comments
 (0)