diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 8a7d3aeb..7caaa7cd 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -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" : { diff --git a/Meshtastic/Helpers/MeshPackets.swift b/Meshtastic/Helpers/MeshPackets.swift index f6b8c485..4c7ab01e 100644 --- a/Meshtastic/Helpers/MeshPackets.swift +++ b/Meshtastic/Helpers/MeshPackets.swift @@ -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 { diff --git a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents index a6e5465f..bc7e7d0b 100644 --- a/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents +++ b/Meshtastic/Meshtastic.xcdatamodeld/MeshtasticDataModelV 55.xcdatamodel/contents @@ -490,10 +490,12 @@ + + diff --git a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift index 480a5cba..39e42baa 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/MapContent/MeshMapContent.swift @@ -177,6 +177,7 @@ struct MeshMapContent: MapContent { } } } + .annotationTitles(.automatic) } } } diff --git a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift index b441bfb4..6054e4af 100644 --- a/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift +++ b/Meshtastic/Views/Nodes/Helpers/Map/WaypointForm.swift @@ -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).. 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).. 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.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.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)") + } + } + } } + + diff --git a/Meshtastic/Views/Nodes/MeshMap.swift b/Meshtastic/Views/Nodes/MeshMap.swift index 3e268afa..b1bf58ba 100644 --- a/Meshtastic/Views/Nodes/MeshMap.swift +++ b/Meshtastic/Views/Nodes/MeshMap.swift @@ -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) }