Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 8 additions & 0 deletions Meshtastic.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@
DD1BD0EB2C601795008C0C70 /* CLLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0EA2C601795008C0C70 /* CLLocation.swift */; };
DD1BD0EE2C603C91008C0C70 /* CustomFormatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0ED2C603C91008C0C70 /* CustomFormatters.swift */; };
DD1BD0F32C63C65E008C0C70 /* SecurityConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */; };
DD0A10022E0292340090CE24 /* NodeDisplayNameStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0A10012E0292330090CE24 /* NodeDisplayNameStore.swift */; };
DD0A10042E0292360090CE24 /* EditNodeDisplayNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD0A10032E0292350090CE24 /* EditNodeDisplayNameView.swift */; };
DD1BEF4A2E0292320090CE24 /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF492E0292220090CE24 /* KeychainHelper.swift */; };
DD1BEF4C2E030D310090CE24 /* KeyBackupStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */; };
DD1BEF4E2E03916A0090CE24 /* ChannelsHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */; };
Expand Down Expand Up @@ -487,6 +489,8 @@
DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = "<group>"; };
DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = "<group>"; };
DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = "<group>"; };
DD0A10012E0292330090CE24 /* NodeDisplayNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDisplayNameStore.swift; sourceTree = "<group>"; };
DD0A10032E0292350090CE24 /* EditNodeDisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditNodeDisplayNameView.swift; sourceTree = "<group>"; };
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = "<group>"; };
DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1340,6 +1344,7 @@
3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */,
BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */,
DDD43FE12A78C86B0083A3E9 /* Mqtt */,
DD0A10012E0292330090CE24 /* NodeDisplayNameStore.swift */,
DD1BEF492E0292220090CE24 /* KeychainHelper.swift */,
DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */,
DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */,
Expand Down Expand Up @@ -1391,6 +1396,7 @@
children = (
DD4C11E02E8099C3003F2F2E /* PreferenceKeys */,
108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */,
DD0A10032E0292350090CE24 /* EditNodeDisplayNameView.swift */,
231B3F232D087C020069A07D /* Metrics Columns */,
DDAD49EB2AFAE82500B4425D /* Map */,
DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */,
Expand Down Expand Up @@ -1791,6 +1797,8 @@
BCB35B4F2E5FC42500B04F60 /* MessageNodeIntent.swift in Sources */,
DDD43FE32A78C8900083A3E9 /* MqttClientProxyManager.swift in Sources */,
BCD7448D2E0F2FAA00F265A2 /* ContactURLHandler.swift in Sources */,
DD0A10022E0292340090CE24 /* NodeDisplayNameStore.swift in Sources */,
DD0A10042E0292360090CE24 /* EditNodeDisplayNameView.swift in Sources */,
DD007BB02AA5981000F5FA12 /* NodeInfoEntityExtension.swift in Sources */,
DDDB26422AABF655003AFCB7 /* NodeListItem.swift in Sources */,
DDDB444629F8A96500EE2349 /* Character.swift in Sources */,
Expand Down
11 changes: 6 additions & 5 deletions Meshtastic/Extensions/CoreData/MessageEntityExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ extension MessageEntity {
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 users.count == 1 {
let name = users.first!.displayLongName
if !name.isEmpty { return name }
}

// If no exact match, find the node with the smallest hopsAway
Expand All @@ -72,8 +72,9 @@ extension MessageEntity {
return false
}
return lhsHops < rhsHops
}), let name = closestNode.longName, !name.isEmpty {
return "\(name)"
}) {
let name = closestNode.displayLongName
if !name.isEmpty { return name }
}

// Fallback to hex node number if no matches
Expand Down
3 changes: 2 additions & 1 deletion Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
import Foundation
import CoreData

extension NodeInfoEntity {
extension NodeInfoEntity: Identifiable {
public var id: NSManagedObjectID { objectID }

var latestPosition: PositionEntity? {
return self.positions?.lastObject as? PositionEntity
Expand Down
18 changes: 18 additions & 0 deletions Meshtastic/Extensions/CoreData/UserEntityExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,24 @@ extension UserEntity {
// Backwards-compatible property (uses viewContext)
var unreadMessages: Int { unreadMessages(context: PersistenceController.shared.container.viewContext) }

/// Local display name for this node (if set), otherwise the device longName.
var displayLongName: String {
if let custom = NodeDisplayNameStore.displayName(for: num) {
return custom
}
return longName ?? "Unknown".localized
}

/// Short label for this node: first 4 characters of display name if set, otherwise device shortName.
var displayShortName: String {
if let custom = NodeDisplayNameStore.displayName(for: num) {
let trimmed = custom.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty { return shortName ?? "?" }
return String(trimmed.prefix(4))
}
return shortName ?? "?"
}

/// SVG Images for Vendors who are signed project backers
var hardwareImage: String? {
guard let hwModel else { return nil }
Expand Down
51 changes: 51 additions & 0 deletions Meshtastic/Helpers/NodeDisplayNameStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// NodeDisplayNameStore.swift
// Meshtastic
//
// Local display names for nodes (keyed by node num). Used only for UI; device identity unchanged.
//

import Foundation

enum NodeDisplayNameStore {
private static let key = "nodeDisplayNames"

/// Posted when any display name is set or cleared so UI can refresh.
static let didChangeNotification = Notification.Name("NodeDisplayNameStoreDidChange")

/// Returns the local display name for a node, or nil if none is set.
static func displayName(for nodeNum: Int64) -> String? {
let all = load()
return all[storageKey(nodeNum)]
}

/// Sets the local display name for a node. Pass nil to clear.
static func setDisplayName(_ name: String?, for nodeNum: Int64) {
var all = load()
let key = storageKey(nodeNum)
if let name = name?.trimmingCharacters(in: .whitespacesAndNewlines), !name.isEmpty {
all[key] = name
} else {
all.removeValue(forKey: key)
}
save(all)
NotificationCenter.default.post(name: didChangeNotification, object: nil)
}

private static func storageKey(_ nodeNum: Int64) -> String {
String(nodeNum)
}

private static func load() -> [String: String] {
guard let data = UserDefaults.standard.data(forKey: key),
let decoded = try? JSONDecoder().decode([String: String].self, from: data) else {
return [:]
}
return decoded
}

private static func save(_ dict: [String: String]) {
guard let data = try? JSONEncoder().encode(dict) else { return }
UserDefaults.standard.set(data, forKey: key)
}
}
4 changes: 2 additions & 2 deletions Meshtastic/Views/Messages/UserList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ fileprivate struct FilteredUserList: View {
.brightness(0.2)
}

CircleText(text: user.shortName ?? "?", color: Color(UIColor(hex: UInt32(user.num))))
CircleText(text: user.displayShortName, color: Color(UIColor(hex: UInt32(user.num))))

VStack(alignment: .leading) {
HStack {
Expand All @@ -131,7 +131,7 @@ fileprivate struct FilteredUserList: View {
Image(systemName: "lock.open.fill")
.foregroundColor(.yellow)
}
Text(user.longName ?? "Unknown".localized)
Text(user.displayLongName)
.font(.headline)
.allowsTightening(true)
Spacer()
Expand Down
60 changes: 60 additions & 0 deletions Meshtastic/Views/Nodes/Helpers/EditNodeDisplayNameView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// EditNodeDisplayNameView.swift
// Meshtastic
//
// Sheet to set or clear a local display name for a node.
//

import SwiftUI
import CoreData

struct EditNodeDisplayNameView: View {
@Environment(\.dismiss) private var dismiss
let node: NodeInfoEntity
@State private var displayName: String = ""
@State private var hasChanges: Bool = false

var body: some View {
NavigationStack {
Form {
Section {
TextField("Display name", text: $displayName)
.autocorrectionDisabled(true)
.onChange(of: displayName) { _, _ in hasChanges = true }
} footer: {
Text("This name is only shown on this device. The node’s real name is unchanged for sharing and export.")
}
if NodeDisplayNameStore.displayName(for: node.num) != nil {
Section {
Button(role: .destructive) {
displayName = ""
hasChanges = true
} label: {
Label("Remove custom name", systemImage: "trash")
}
}
}
}
.navigationTitle("Display name")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel") {
dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Save") {
let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
NodeDisplayNameStore.setDisplayName(trimmed.isEmpty ? nil : trimmed, for: node.num)
dismiss()
}
.disabled(!hasChanges)
}
}
.onAppear {
displayName = NodeDisplayNameStore.displayName(for: node.num) ?? ""
}
}
}
}
47 changes: 37 additions & 10 deletions Meshtastic/Views/Nodes/Helpers/NodeDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ struct NodeDetail: View {
@ObservedObject var node: NodeInfoEntity
@State private var environmentSectionHeight: CGFloat = 0
@State var showingCompassSheet = false

@State private var showingDisplayNameSheet = false
@State private var displayNameRefresh = 0

var body: some View {
NavigationStack {
ScrollViewReader { scrollView in
Expand All @@ -49,11 +51,11 @@ struct NodeDetail: View {
Section("Node") { // Node
HStack(alignment: .center) {
Spacer()
CircleText(
text: node.user?.shortName ?? "?",
color: Color(UIColor(hex: UInt32(node.num))),
circleSize: 75
)
CircleText(
text: node.user?.displayShortName ?? "?",
color: Color(UIColor(hex: UInt32(node.num))),
circleSize: 75
)
if node.snr != 0 && !node.viaMqtt && node.hopsAway == 0 {
Spacer()
VStack {
Expand Down Expand Up @@ -120,6 +122,23 @@ struct NodeDetail: View {
.textSelection(.enabled)
}
.accessibilityElement(children: .combine)
Button {
showingDisplayNameSheet = true
} label: {
HStack {
Label {
Text("Display name")
} icon: {
Image(systemName: "pencil.circle")
.symbolRenderingMode(.hierarchical)
}
Spacer()
Text(node.user?.displayLongName ?? "—")
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
.accessibilityElement(children: .combine)
let connectedNode = getNodeInfo(id: accessoryManager.activeDeviceNum ?? 0, context: context)
if let user = node.user, user.keyMatch {
let publicKey = node.num == connectedNode?.num
Expand Down Expand Up @@ -575,14 +594,22 @@ struct NodeDetail: View {
}
}
}
.sheet(isPresented: $showingCompassSheet) {
CompassView(waypointLocation: node.latestPosition?.nodeCoordinate ?? nil, waypointName: node.user?.longName ?? nil, color: Color(UIColor(hex: UInt32(node.num))))
}
.sheet(isPresented: $showingCompassSheet) {
CompassView(waypointLocation: node.latestPosition?.nodeCoordinate ?? nil, waypointName: node.user?.displayLongName, color: Color(UIColor(hex: UInt32(node.num))))
}
.sheet(isPresented: $showingDisplayNameSheet) {
EditNodeDisplayNameView(node: node)
.onDisappear { displayNameRefresh += 1 }
}
.onReceive(NotificationCenter.default.publisher(for: NodeDisplayNameStore.didChangeNotification)) { _ in
displayNameRefresh += 1
}
.onAppear {
scrollView.scrollTo("topOfList", anchor: .top)
}
.listStyle(.insetGrouped)
.navigationTitle(String(node.user?.longName?.addingVariationSelectors ?? "Unknown".localized))
.navigationTitle(String((node.user?.displayLongName ?? "Unknown".localized).addingVariationSelectors))
.id(displayNameRefresh)
.navigationBarTitleDisplayMode(.inline)
}
}
Expand Down
22 changes: 10 additions & 12 deletions Meshtastic/Views/Nodes/Helpers/NodeListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ struct NodeListItem: View {

private var accessibilityDescription: String {
var desc = ""
if let shortName = node.user?.shortName {
desc = shortName.formatNodeNameForVoiceOver()
} else if let longName = node.user?.longName {
desc = longName
} else {
desc = "Unknown".localized + " " + "Node".localized
}
let shortName = node.user?.displayShortName ?? "?"
let longName = node.user?.displayLongName ?? "Unknown".localized
desc = shortName.formatNodeNameForVoiceOver()
if desc.isEmpty { desc = longName }
if desc.isEmpty { desc = "Unknown".localized + " " + "Node".localized }
if isDirectlyConnected {
desc += ", currently connected"
}
Expand Down Expand Up @@ -128,7 +126,7 @@ struct NodeListItem: View {
LazyVStack(alignment: .leading) {
HStack {
VStack(alignment: .center) {
CircleText(text: node.user?.shortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70)
CircleText(text: node.user?.displayShortName ?? "?", color: Color(UIColor(hex: UInt32(node.num))), circleSize: 70)
.padding(.trailing, 5)
if node.latestDeviceMetrics != nil {
BatteryCompact(batteryLevel: node.latestDeviceMetrics?.batteryLevel ?? 0, font: .caption, iconFont: .callout, color: .accentColor)
Expand All @@ -138,10 +136,10 @@ struct NodeListItem: View {
VStack(alignment: .leading) {
HStack {
let (image, color) = userKeyStatus
IconAndText(systemName: image,
imageColor: color,
text: node.user?.longName?.addingVariationSelectors ?? "Unknown".localized,
textColor: .primary)
IconAndText(systemName: image,
imageColor: color,
text: (node.user?.displayLongName ?? "Unknown".localized).addingVariationSelectors,
textColor: .primary)
if node.favorite {
Spacer()
Image(systemName: "star.fill")
Expand Down
Loading