Show which node created a waypoint and which last updated by (#1496)

* Fixed some issues with waypoints and created a createdBy and lastUpdatedBy

* Fix suggestions

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
Benjamin Faershtein 2026-04-04 18:10:45 -07:00 committed by GitHub
parent c388bf9b40
commit 7886fdc7c0
6 changed files with 401 additions and 296 deletions

View file

@ -13802,6 +13802,9 @@
}
}
}
},
"Created by:" : {
},
"Created: %@" : {
"localizations" : {
@ -28406,6 +28409,9 @@
}
}
}
},
"Last updated by:" : {
},
"Later" : {
"comment" : "A button that dismisses an alert without taking any action.",
@ -31372,6 +31378,10 @@
"comment" : "A description of the read-only mode feature in TAK Server.",
"isCommentAutoGenerated" : true
},
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. This helps us understand how the app is being used and where we can make improvements. The data we collect is non-personally identifiable and cannot be linked to you as an individual. You can opt out of this under app settings." : {
"comment" : "Privacy policy text for Meshtastic.",
"isCommentAutoGenerated" : true
},
"Meshtastic does not collect any personal information. We do anonymously collect usage and crash data to improve the app. You can opt out under app settings." : {
"localizations" : {
"es" : {

View file

@ -1175,7 +1175,7 @@ actor MeshPackets {
// Fetch waypoint by waypointMessage.id, not packet.id
let fetchWaypointRequest = WaypointEntity.fetchRequest()
fetchWaypointRequest.predicate = NSPredicate(format: "id == %lld", Int64(waypointMessage.id))
let fetchedWaypoint = try context.fetch(fetchWaypointRequest)
// Fetch the node info to get the short name
var nodeShortName: String = "?"
@ -1199,6 +1199,7 @@ actor MeshPackets {
waypoint.longitudeI = waypointMessage.longitudeI
waypoint.icon = Int64(waypointMessage.icon)
waypoint.locked = Int64(waypointMessage.lockedTo)
waypoint.createdBy = Int64(packet.from)
if waypointMessage.expire >= 1 {
waypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
} else {
@ -1254,6 +1255,7 @@ actor MeshPackets {
existingWaypoint.longitudeI = waypointMessage.longitudeI
existingWaypoint.icon = Int64(waypointMessage.icon)
existingWaypoint.locked = Int64(waypointMessage.lockedTo)
existingWaypoint.lastUpdatedBy = Int64(packet.from)
if waypointMessage.expire >= 1 {
existingWaypoint.expire = Date(timeIntervalSince1970: TimeInterval(Int64(waypointMessage.expire)))
} else {

View file

@ -490,10 +490,12 @@
</entity>
<entity name="WaypointEntity" representedClassName="WaypointEntity" syncable="YES" codeGenerationType="class">
<attribute name="created" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="createdBy" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="expire" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="icon" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="lastUpdated" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lastUpdatedBy" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="latitudeI" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="locked" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="longDescription" optional="YES" attributeType="String" maxValueString="100"/>

View file

@ -177,6 +177,7 @@ struct MeshMapContent: MapContent {
}
}
}
.annotationTitles(.automatic)
}
}
}

View file

@ -10,6 +10,7 @@ import MapKit
import MeshtasticProtobufs
import OSLog
import SwiftUI
import CoreData
struct WaypointForm: View {
@ -31,134 +32,218 @@ struct WaypointForm: View {
@State private var lockedTo: Int64 = 0
@State private var selectedDetent: PresentationDetent = .medium
@State private var waypointFailedAlert: Bool = false
@State private var createdByNode : NodeInfoEntity? = nil
@State private var lastUpdatedByNode : NodeInfoEntity? = nil
var body: some View {
NavigationStack {
if editMode {
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
.font(.largeTitle)
Divider()
Form {
if let cl = LocationsHandler.currentLocation {
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
Section(header: Text("Coordinate") ) {
Group {
if editMode {
Text((waypoint.id > 0) ? "Editing Waypoint" : "Create Waypoint")
.font(.largeTitle)
Divider()
Form {
if let cl = LocationsHandler.currentLocation {
let distance = CLLocation(latitude: cl.latitude, longitude: cl.longitude).distance(from: CLLocation(latitude: waypoint.coordinate.latitude, longitude: waypoint.coordinate.longitude ))
Section(header: Text("Coordinate") ) {
HStack {
Text("Location:")
.foregroundColor(.secondary)
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption)
}
Button {
waypoint.coordinate.longitude = cl.longitude
waypoint.coordinate.latitude = cl.latitude
} label: {
HStack {
Text("Location:")
.foregroundColor(.secondary)
Text("\(String(format: "%.5f", waypoint.coordinate.latitude) + "," + String(format: "%.5f", waypoint.coordinate.longitude))")
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption)
}
Button {
waypoint.coordinate.longitude = cl.longitude
waypoint.coordinate.latitude = cl.latitude
} label: {
HStack {
Text("Use my Location")
Image(systemName: "location")
}
}
.accessibilityLabel("Set to current location")
HStack {
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
DistanceText(meters: distance)
.foregroundColor(Color.gray)
}
Text("Use my Location")
Image(systemName: "location")
}
}
}
Section(header: Text("Waypoint Options")) {
.accessibilityLabel("Set to current location")
HStack {
Text("Name")
Spacer()
TextField(
"Name",
text: $name,
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: name) {
var totalBytes = name.utf8.count
// Only mess with the value if it is too big
while totalBytes > 30 {
name = String(name.dropLast())
totalBytes = name.utf8.count
}
waypoint.name = name.count > 0 ? name : "Dropped Pin"
if waypoint.coordinate.latitude != 0 && waypoint.coordinate.longitude != 0 {
DistanceText(meters: distance)
.foregroundColor(Color.gray)
}
}
HStack {
Text("Description")
Spacer()
TextField(
"Description",
text: $description,
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: description) {
var totalBytes = description.utf8.count
// Only mess with the value if it is too big
while totalBytes > 100 {
description = String(description.dropLast())
totalBytes = description.utf8.count
}
}
}
HStack {
Text("Icon")
Spacer()
TextField("Select an emoji", text: $icon)
.keyboardType(.emoji)
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) { _, value in
// If a second emoji is entered delete the first one
if value.count >= 1 {
if value.count > 1 {
let index = value.index(value.startIndex, offsetBy: 1)
icon = String(value[index])
}
}
}
}
Toggle(isOn: $expires) {
Label("Expires", systemImage: "clock.badge.xmark")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if expires {
DatePicker("Expire", selection: $expire, in: Date.now...)
.datePickerStyle(.compact)
.font(.callout)
}
Toggle(isOn: $locked) {
Label("Locked", systemImage: "lock")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
.scrollDismissesKeyboard(.immediately)
HStack {
Button {
guard let deviceNum = accessoryManager.activeDeviceNum else {
Logger.mesh.warning("Send waypoint failed: No deviceNum")
return
}
if accessoryManager.isConnected {
/// Send a new or exiting waypoint
var newWaypoint = Waypoint()
if waypoint.id == 0 {
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
waypoint.id = Int64(newWaypoint.id)
} else {
newWaypoint.id = UInt32(waypoint.id)
Section(header: Text("Waypoint Options")) {
HStack {
Text("Name")
Spacer()
TextField(
"Name",
text: $name,
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: name) {
var totalBytes = name.utf8.count
// Only mess with the value if it is too big
while totalBytes > 30 {
name = String(name.dropLast())
totalBytes = name.utf8.count
}
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
waypoint.name = name.count > 0 ? name : "Dropped Pin"
}
}
HStack {
Text("Description")
Spacer()
TextField(
"Description",
text: $description,
axis: .vertical
)
.foregroundColor(Color.gray)
.onChange(of: description) {
var totalBytes = description.utf8.count
// Only mess with the value if it is too big
while totalBytes > 100 {
description = String(description.dropLast())
totalBytes = description.utf8.count
}
}
}
HStack {
Text("Icon")
Spacer()
EmojiOnlyTextField(text: $icon, placeholder: "Select an emoji")
.font(.title)
.focused($iconIsFocused)
.onChange(of: icon) {
// If it contains non-emoji characters, clear it
if !icon.onlyEmojis() {
icon = ""
return
}
// If multiple emojis are entered or pasted, keep only the last one
if icon.count > 1 {
icon = String(icon.suffix(1))
}
iconIsFocused = false
}
}
Toggle(isOn: $expires) {
Label("Expires", systemImage: "clock.badge.xmark")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
if expires {
DatePicker("Expire", selection: $expire, in: Date.now...)
.datePickerStyle(.compact)
.font(.callout)
}
Toggle(isOn: $locked) {
Label("Locked", systemImage: "lock")
}
.toggleStyle(SwitchToggleStyle(tint: .accentColor))
}
}
.scrollContentBackground(.hidden)
.scrollDismissesKeyboard(.immediately)
HStack {
Button {
guard let deviceNum = accessoryManager.activeDeviceNum else {
Logger.mesh.warning("Send waypoint failed: No deviceNum")
return
}
if accessoryManager.isConnected {
/// Send a new or exiting waypoint
var newWaypoint = Waypoint()
if waypoint.id == 0 {
newWaypoint.id = UInt32.random(in: UInt32(UInt8.max)..<UInt32.max)
waypoint.createdBy = Int64(deviceNum)
waypoint.id = Int64(newWaypoint.id)
} else {
waypoint.lastUpdatedBy = Int64(deviceNum)
newWaypoint.id = UInt32(waypoint.id)
}
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
if locked {
if lockedTo == 0 {
newWaypoint.lockedTo = UInt32(deviceNum)
} else {
newWaypoint.lockedTo = UInt32(lockedTo)
}
}
if expires {
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
} else {
newWaypoint.expire = 0
}
Task {
do {
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
dismiss()
} catch {
Logger.mesh.warning("Send waypoint failed: \(error)")
Task { @MainActor in
waypointFailedAlert = true
}
}
}
} else {
Logger.mesh.warning("Send waypoint failed, node not connected")
}
} label: {
Label("Send", systemImage: "arrow.up")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.regular)
.disabled(!accessoryManager.isConnected)
.padding(.bottom)
Button(role: .cancel) {
dismiss()
} label: {
Label("Cancel", systemImage: "x.circle")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.regular)
.padding(.bottom)
if waypoint.id > 0 && accessoryManager.isConnected {
Menu {
Button("For me", action: {
context.delete(waypoint)
do {
try context.save()
} catch {
context.rollback()
}
dismiss() })
Button("For everyone", action: {
guard let deviceNum = accessoryManager.activeDeviceNum else {
Logger.mesh.error("Unable to set waypoint: No Device num")
return
}
var newWaypoint = Waypoint()
newWaypoint.id = UInt32(waypoint.id)
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
@ -171,101 +256,28 @@ struct WaypointForm: View {
newWaypoint.lockedTo = UInt32(lockedTo)
}
}
if expires {
newWaypoint.expire = UInt32(expire.timeIntervalSince1970)
} else {
newWaypoint.expire = 0
}
newWaypoint.expire = UInt32(1)
Task {
do {
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
dismiss()
} catch {
Logger.mesh.warning("Send waypoint failed: \(error)")
Task { @MainActor in
context.delete(waypoint)
do {
try context.save()
} catch {
context.rollback()
}
dismiss()
}
} catch {
Logger.mesh.warning("Send waypoint failed")
Task {@MainActor in
waypointFailedAlert = true
}
}
}
} else {
Logger.mesh.warning("Send waypoint failed, node not connected")
}
} label: {
Label("Send", systemImage: "arrow.up")
})
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.regular)
.disabled(!accessoryManager.isConnected)
.padding(.bottom)
Button(role: .cancel) {
dismiss()
} label: {
Label("Cancel", systemImage: "x.circle")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.regular)
.padding(.bottom)
if waypoint.id > 0 && accessoryManager.isConnected {
Menu {
Button("For me", action: {
context.delete(waypoint)
do {
try context.save()
} catch {
context.rollback()
}
dismiss() })
Button("For everyone", action: {
guard let deviceNum = accessoryManager.activeDeviceNum else {
Logger.mesh.error("Unable to set waypoint: No Device num")
return
}
var newWaypoint = Waypoint()
newWaypoint.id = UInt32(waypoint.id)
newWaypoint.name = name.count > 0 ? name : "Dropped Pin"
newWaypoint.description_p = description
newWaypoint.latitudeI = waypoint.latitudeI
newWaypoint.longitudeI = waypoint.longitudeI
// Unicode scalar value for the icon emoji string
let unicodeScalers = icon.unicodeScalars
// First element as an UInt32
let unicode = unicodeScalers[unicodeScalers.startIndex].value
newWaypoint.icon = unicode
if locked {
if lockedTo == 0 {
newWaypoint.lockedTo = UInt32(deviceNum)
} else {
newWaypoint.lockedTo = UInt32(lockedTo)
}
}
newWaypoint.expire = UInt32(1)
Task {
do {
try await accessoryManager.sendWaypoint(waypoint: newWaypoint)
Task { @MainActor in
context.delete(waypoint)
do {
try context.save()
} catch {
context.rollback()
}
dismiss()
}
} catch {
Logger.mesh.warning("Send waypoint failed")
Task {@MainActor in
waypointFailedAlert = true
}
}
}
})
}
label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.red)
@ -274,130 +286,167 @@ struct WaypointForm: View {
.buttonBorderShape(.capsule)
.controlSize(.regular)
.padding(.bottom)
}
}
} else {
VStack {
HStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
Spacer()
Text(waypoint.name ?? "?")
.font(.largeTitle)
Spacer()
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
Image(systemName: "lock.fill")
.font(.largeTitle)
} else {
Button {
editMode = true
selectedDetent = .fraction(0.85)
} label: {
Image(systemName: "square.and.pencil" )
.font(.largeTitle)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
}
}
}
} else {
VStack {
HStack {
CircleText(text: String(UnicodeScalar(Int(waypoint.icon)) ?? "📍"), color: Color.orange, circleSize: 50)
Spacer()
Text(waypoint.name ?? "?")
.font(.largeTitle)
Spacer()
if waypoint.locked > 0 && waypoint.locked != UInt32(accessoryManager.activeDeviceNum ?? 0) {
Image(systemName: "lock.fill")
.font(.largeTitle)
} else {
Button {
editMode = true
selectedDetent = .fraction(0.85)
} label: {
Image(systemName: "square.and.pencil" )
.font(.largeTitle)
.symbolRenderingMode(.hierarchical)
.foregroundColor(.accentColor)
Divider()
VStack(alignment: .leading) {
// Nodes who created/modified
VStack(alignment: .leading, spacing: 12) {
if let created = createdByNode {
VStack(alignment: .leading, spacing: 6) {
Text("Created by:")
.font(.headline)
HStack(spacing: 8) {
CircleText(
text: created.user?.shortName ?? "?",
color: Color(UIColor(hex: UInt32(created.user?.num ?? 0x808080)))
)
Text(created.user?.longName ?? "Unknown")
.font(.body)
}
}
}
if let updated = lastUpdatedByNode {
VStack(alignment: .leading, spacing: 6) {
Text("Last updated by:")
.font(.headline)
HStack(spacing: 8) {
CircleText(
text: updated.user?.shortName ?? "?",
color: Color(UIColor(hex: UInt32(updated.user?.num ?? 0x808080)))
)
Text(updated.user?.longName ?? "Unknown")
.font(.body)
}
}
}
}
Divider()
VStack(alignment: .leading) {
// Description
if (waypoint.longDescription ?? "").count > 0 {
Label {
Text(waypoint.longDescription ?? "")
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
} icon: {
Image(systemName: "doc.plaintext")
}
.padding(.bottom)
}
/// Coordinate
.padding(.bottom)
// Description
if (waypoint.longDescription ?? "").count > 0 {
Label {
Text("Coordinates:")
Text(waypoint.longDescription ?? "")
.foregroundColor(.primary)
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption2)
} icon: {
Image(systemName: "mappin.circle")
Image(systemName: "doc.plaintext")
}
.padding(.bottom)
// Drop Maps Pin
Button(action: {
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
UIApplication.shared.open(url)
}
}) {
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
}
/// Coordinate
Label {
Text("Coordinates:")
.foregroundColor(.primary)
Text("\(String(format: "%.6f", waypoint.coordinate.latitude)), \(String(format: "%.6f", waypoint.coordinate.longitude))")
.textSelection(.enabled)
.foregroundColor(.secondary)
.font(.caption2)
} icon: {
Image(systemName: "mappin.circle")
}
.padding(.bottom)
// Drop Maps Pin
Button(action: {
if let url = URL(string: "http://maps.apple.com/?ll=\(waypoint.coordinate.latitude),\(waypoint.coordinate.longitude)&q=\(waypoint.name ?? "Dropped Pin")") {
UIApplication.shared.open(url)
}
.padding(.bottom)
/// Created
}) {
Label("Drop Pin in Maps", systemImage: "mappin.and.ellipse")
}
.padding(.bottom)
/// Created
Label {
Text("Created: \(waypoint.created?.formatted() ?? "?")")
.foregroundColor(.primary)
} icon: {
Image(systemName: "clock.badge.checkmark")
.symbolRenderingMode(.hierarchical)
}
.padding(.bottom)
/// Updated
if waypoint.lastUpdated != nil {
Label {
Text("Created: \(waypoint.created?.formatted() ?? "?")")
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
.foregroundColor(.primary)
} icon: {
Image(systemName: "clock.badge.checkmark")
Image(systemName: "clock.arrow.circlepath")
.symbolRenderingMode(.hierarchical)
}
.padding(.bottom)
/// Updated
if waypoint.lastUpdated != nil {
Label {
Text("Updated: \(waypoint.lastUpdated?.formatted() ?? "?")")
.foregroundColor(.primary)
} icon: {
Image(systemName: "clock.arrow.circlepath")
.symbolRenderingMode(.hierarchical)
}
.padding(.bottom)
}
/// Expires
if waypoint.expire != nil {
Label {
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
.foregroundColor(.primary)
} icon: {
Image(systemName: "hourglass.bottomhalf.filled")
.symbolRenderingMode(.hierarchical)
}
/// Expires
if waypoint.expire != nil {
.padding(.bottom, 5)
}
/// Distance
if let cl = LocationsHandler.currentLocation {
if cl.distance(from: cl) > 0.0 {
let metersAway = waypoint.coordinate.distance(from: cl)
Label {
Text("Expires: \(waypoint.expire?.formatted() ?? "?")")
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
} icon: {
Image(systemName: "hourglass.bottomhalf.filled")
Image(systemName: "lines.measurement.horizontal")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
}
/// Distance
if let cl = LocationsHandler.currentLocation {
if cl.distance(from: cl) > 0.0 {
let metersAway = waypoint.coordinate.distance(from: cl)
Label {
Text("Distance".localized + ": \(distanceFormatter.string(fromDistance: Double(metersAway)))")
.foregroundColor(.primary)
} icon: {
Image(systemName: "lines.measurement.horizontal")
.symbolRenderingMode(.hierarchical)
.frame(width: 35)
}
.padding(.bottom, 5)
}
}
}
.padding(.top)
#if targetEnvironment(macCatalyst)
Spacer()
Button {
dismiss()
} label: {
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
#endif
}
.padding(.top)
#if targetEnvironment(macCatalyst)
Spacer()
Button {
dismiss()
} label: {
Label("Close", systemImage: "xmark")
}
.buttonStyle(.bordered)
.buttonBorderShape(.capsule)
.controlSize(.large)
.padding()
#endif
}
}
}
.alert("Waypoint Failed to Send", isPresented: $waypointFailedAlert) {
Button("OK", role: .cancel) {
context.delete(waypoint)
@ -421,6 +470,9 @@ struct WaypointForm: View {
}
}
}
.task {
await fetchNodeInfo()
}
.onAppear {
if waypoint.id > 0 {
let waypoint = getWaypoint(id: Int64(waypoint.id), context: context)
@ -453,4 +505,37 @@ struct WaypointForm: View {
.presentationBackgroundInteraction(.enabled(upThrough: .fraction(0.85)))
.presentationDragIndicator(.visible)
}
private func fetchNodeInfo() async {
// --- Fetch createdBy node ---
if waypoint.createdBy != 0 {
let createdByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
createdByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.createdBy))
createdByFetch.fetchLimit = 1
do {
let nodes = try context.fetch(createdByFetch)
createdByNode = nodes.first
} catch {
Logger.services.warning("Error fetching createdBy node: \(error.localizedDescription)")
}
}
// --- Fetch lastUpdatedBy node (only if different from createdBy) ---
if waypoint.lastUpdatedBy != 0,
waypoint.lastUpdatedBy != waypoint.createdBy {
let updatedByFetch: NSFetchRequest<NodeInfoEntity> = NodeInfoEntity.fetchRequest()
updatedByFetch.predicate = NSPredicate(format: "num == %lld", Int64(waypoint.lastUpdatedBy))
updatedByFetch.fetchLimit = 1
do {
let nodes = try context.fetch(updatedByFetch)
lastUpdatedByNode = nodes.first
} catch {
Logger.services.warning("Error fetching lastUpdatedBy node: \(error.localizedDescription)")
}
}
}
}

View file

@ -120,12 +120,17 @@ struct MeshMap: View {
}
.sheet(item: $selectedWaypoint) { selection in
WaypointForm(waypoint: selection)
.presentationDetents([.large])
.padding()
.presentationDetents([.large]) // full screen
.presentationDragIndicator(.visible)
}
.sheet(item: $editingWaypoint) { selection in
WaypointForm(waypoint: selection, editMode: true)
.padding()
.presentationDetents([.large])
.presentationDragIndicator(.visible)
}
.sheet(isPresented: $editingSettings) {
MapSettingsForm(traffic: $showTraffic, pointsOfInterest: $showPointsOfInterest, mapLayer: $selectedMapLayer, meshMap: $isMeshMap, enabledOverlayConfigs: $enabledOverlayConfigs)
}