Show who relayed messages (#1486)

* Add identification for node that relayed text messages and add count for ammount of relayers of your message

* Ack Relays
This commit is contained in:
Benjamin Faershtein 2025-10-28 06:19:12 -07:00 committed by GitHub
parent 3f27e3b925
commit 7668a7a7ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 14 deletions

View file

@ -28480,6 +28480,7 @@
}
},
"Received Ack" : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@ -28542,8 +28543,12 @@
}
}
}
},
"Received Ack: %@" : {
},
"Recipient Ack" : {
"extractionState" : "stale",
"localizations" : {
"de" : {
"stringUnit" : {
@ -28606,6 +28611,9 @@
}
}
}
},
"Recipient Ack: %@" : {
},
"Recording route" : {
"localizations" : {
@ -28789,6 +28797,16 @@
}
}
},
"Relayed by %d %@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Relayed by %1$d %2$@"
}
}
}
},
"Release Notes" : {
"localizations" : {
"it" : {

View file

@ -38,4 +38,39 @@ extension MessageEntity {
}
return false // First message will have no timestamp
}
func relayDisplay() -> String? {
guard self.relayNode != 0 else { return nil }
let context = PersistenceController.shared.container.viewContext
let relaySuffix = Int64(self.relayNode & 0xFF)
let request: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
request.predicate = NSPredicate(format: "(num & 0xFF) == %lld", relaySuffix)
do {
let users = try context.fetch(request)
// If exactly one match is found, return its name
if users.count == 1, let name = users.first?.longName, !name.isEmpty {
return "\(name)"
}
// If no exact match, find the node with the smallest hopsAway
if let closestNode = users.min(by: { lhs, rhs in
guard let lhsHops = lhs.userNode?.hopsAway, let rhsHops = rhs.userNode?.hopsAway else {
return false
}
return lhsHops < rhsHops
}), let name = closestNode.longName, !name.isEmpty {
return "\(name)"
}
// Fallback to hex node number if no matches
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
} catch {
return String(format: "Node 0x%02X", UInt32(self.relayNode & 0xFF))
}
}
}

View file

@ -624,6 +624,7 @@ func adminResponseAck (packet: MeshPacket, context: NSManagedObjectContext) {
fetchedMessage[0].ackError = Int32(RoutingError.none.rawValue)
fetchedMessage[0].receivedACK = true
fetchedMessage[0].realACK = true
fetchedMessage[0].relayNode = Int64(packet.relayNode)
fetchedMessage[0].ackSNR = packet.rxSnr
if fetchedMessage[0].fromUser != nil {
fetchedMessage[0].fromUser?.objectWillChange.send()
@ -699,9 +700,11 @@ func routingPacket (packet: MeshPacket, connectedNodeNum: Int64, context: NSMana
fetchedMessage[0].realACK = true
}
}
fetchedMessage[0].relayNode = Int64(packet.relayNode)
fetchedMessage[0].ackError = Int32(routingMessage.errorReason.rawValue)
if routingMessage.errorReason == Routing.Error.none {
fetchedMessage[0].receivedACK = true
fetchedMessage[0].relays += 1
}
fetchedMessage[0].ackSNR = packet.rxSnr
@ -944,6 +947,9 @@ func textMessageAppPacket(
} else {
newMessage.messageTimestamp = Int32(Date().timeIntervalSince1970)
}
if packet.relayNode != 0 {
newMessage.relayNode = Int64(packet.relayNode)
}
newMessage.receivedACK = false
newMessage.snr = packet.rxSnr
newMessage.rssi = packet.rxRssi
@ -983,6 +989,7 @@ func textMessageAppPacket(
newMessage.pkiEncrypted = true
newMessage.publicKey = packet.publicKey
}
/// Check for key mismatch
if let nodeKey = newMessage.fromUser?.publicKey {
if newMessage.toUser != nil && packet.pkiEncrypted && !packet.publicKey.isEmpty {

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24299" systemVersion="25A354" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24299" systemVersion="25A362" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AmbientLightingConfigEntity" representedClassName="AmbientLightingConfigEntity" syncable="YES" codeGenerationType="class">
<attribute name="blue" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="current" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
@ -164,6 +164,8 @@
<attribute name="read" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="realACK" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="receivedACK" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="relayNode" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="relays" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="replyID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="rssi" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="snr" optional="YES" attributeType="Float" defaultValueString="0.0" usesScalarValueType="YES"/>

View file

@ -11,6 +11,7 @@ struct MessageContextMenuItems: View {
let isCurrentUser: Bool
@Binding var isShowingDeleteConfirmation: Bool
let onReply: () -> Void
@State var relayDisplay: String? = nil
var body: some View {
VStack {
@ -19,6 +20,14 @@ struct MessageContextMenuItems: View {
}
Text("Channel") + Text(": \(message.channel)")
}
.onAppear {
DispatchQueue.global(qos: .userInitiated).async {
let result = message.relayDisplay()
DispatchQueue.main.async {
relayDisplay = result
}
}
}
Menu("Tapback") {
ForEach(Tapbacks.allCases) { tb in
@ -59,12 +68,27 @@ struct MessageContextMenuItems: View {
}
Menu("Message Details") {
// Precompute values to avoid executing non-View code inside the ViewBuilder
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
// Compute a relay display string if relayNode is present
VStack {
let messageDate = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))").foregroundColor(.gray)
Text("\(messageDate.formattedDate(format: MessageText.dateFormatString))")
.foregroundColor(.gray)
}
if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 {
if let relayDisplay {
let prefix = message.realACK ? "Ack Relay: " : "Relay: "
Text(prefix + relayDisplay)
.foregroundColor(relayDisplay.contains("Node ") ? .gray : .primary)
.font(relayDisplay.contains("Node ") ? .caption : .body)
}
if !isCurrentUser && !(message.fromUser?.userNode?.viaMqtt ?? false) && message.fromUser?.userNode?.hopsAway ?? -1 == 0 {
VStack {
Text("SNR \(String(format: "%.2f", message.snr)) dB")
Text("RSSI \(String(format: "%.2f", message.rssi)) dBm")
@ -74,29 +98,29 @@ struct MessageContextMenuItems: View {
Text("Hops Away \(message.fromUser?.userNode?.hopsAway ?? 0)")
}
}
if message.relays != 0 && message.realACK == false {
Text("Relayed by \(message.relays) \(message.relays == 1 ? "node" : "nodes")")
}
if isCurrentUser && message.receivedACK {
VStack {
Text("Received Ack") + Text(": \(message.receivedACK ? "✔️" : "")")
Text("Recipient Ack") + Text(": \(message.realACK ? "✔️" : "")")
Text("Received Ack: \(message.receivedACK ? "✔️" : "")")
Text("Recipient Ack: \(message.realACK ? "✔️" : "")")
}
} else if isCurrentUser && message.ackError == 0 {
// Empty Error
Text("Waiting")
} else if isCurrentUser && message.ackError > 0 {
let ackErrorVal = RoutingError(rawValue: Int(message.ackError))
Text("\(ackErrorVal?.display ?? "Empty Ack Error")")
.fixedSize(horizontal: false, vertical: true)
}
if isCurrentUser {
VStack {
let ackDate = Date(timeIntervalSince1970: TimeInterval(message.ackTimestamp))
let sixMonthsAgo = Calendar.current.date(byAdding: .month, value: -6, to: Date())
if ackDate >= sixMonthsAgo! {
Text("Ack Time: \(ackDate.formattedDate(format: MessageText.timeFormatString))")
.foregroundColor(.gray)
}
if let sixMonthsAgo, ackDate >= sixMonthsAgo {
Text("Ack Time: \(ackDate.formattedDate(format: MessageText.timeFormatString))")
.foregroundColor(.gray)
}
}
if message.ackSNR != 0 {
VStack {
Text("Ack SNR: \(String(format: "%.2f", message.ackSNR)) dB")