From 59ff5ba96a18d0989b8fea95d7259f41a976a681 Mon Sep 17 00:00:00 2001 From: Meshtastic Contributor Date: Sun, 8 Mar 2026 20:44:00 +0000 Subject: [PATCH] feat: local display names for nodes - Add NodeDisplayNameStore (UserDefaults) keyed by node number - Add displayLongName/displayShortName on UserEntity - Show custom name in node list, detail, messages, relay text - Add EditNodeDisplayNameView sheet and 'Set display name' in list/detail - Notify UI on change via NodeDisplayNameStore.didChangeNotification Made-with: Cursor --- Meshtastic.xcodeproj/project.pbxproj | 8 +++ .../CoreData/MessageEntityExtension.swift | 11 ++-- .../CoreData/NodeInfoEntityExtension.swift | 3 +- .../CoreData/UserEntityExtension.swift | 18 ++++++ Meshtastic/Helpers/NodeDisplayNameStore.swift | 51 ++++++++++++++++ Meshtastic/Views/Messages/UserList.swift | 4 +- .../Helpers/EditNodeDisplayNameView.swift | 60 +++++++++++++++++++ .../Views/Nodes/Helpers/NodeDetail.swift | 47 +++++++++++---- .../Views/Nodes/Helpers/NodeListItem.swift | 22 ++++--- Meshtastic/Views/Nodes/NodeList.swift | 25 ++++++-- Meshtastic/Views/Nodes/NodeRow.swift | 16 ++--- 11 files changed, 221 insertions(+), 44 deletions(-) create mode 100644 Meshtastic/Helpers/NodeDisplayNameStore.swift create mode 100644 Meshtastic/Views/Nodes/Helpers/EditNodeDisplayNameView.swift diff --git a/Meshtastic.xcodeproj/project.pbxproj b/Meshtastic.xcodeproj/project.pbxproj index 1544d0eb..aaaa452c 100644 --- a/Meshtastic.xcodeproj/project.pbxproj +++ b/Meshtastic.xcodeproj/project.pbxproj @@ -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 */; }; @@ -487,6 +489,8 @@ DD1BD0F12C61D3AD008C0C70 /* MeshtasticDataModelV 42.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 42.xcdatamodel"; sourceTree = ""; }; DD1BD0F22C63C65E008C0C70 /* SecurityConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityConfig.swift; sourceTree = ""; }; DD1BEF462DFF284C0090CE24 /* MeshtasticDataModelV 53.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "MeshtasticDataModelV 53.xcdatamodel"; sourceTree = ""; }; + DD0A10012E0292330090CE24 /* NodeDisplayNameStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeDisplayNameStore.swift; sourceTree = ""; }; + DD0A10032E0292350090CE24 /* EditNodeDisplayNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditNodeDisplayNameView.swift; sourceTree = ""; }; DD1BEF492E0292220090CE24 /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = ""; }; DD1BEF4B2E030D240090CE24 /* KeyBackupStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBackupStatus.swift; sourceTree = ""; }; DD1BEF4D2E0391620090CE24 /* ChannelsHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelsHelp.swift; sourceTree = ""; }; @@ -1340,6 +1344,7 @@ 3D3417C72E29D38A006A988B /* GeoJSONOverlayConfig.swift */, BCD7448C2E0F2FA300F265A2 /* ContactURLHandler.swift */, DDD43FE12A78C86B0083A3E9 /* Mqtt */, + DD0A10012E0292330090CE24 /* NodeDisplayNameStore.swift */, DD1BEF492E0292220090CE24 /* KeychainHelper.swift */, DD913638270DFF4C00D7ACF3 /* LocalNotificationManager.swift */, DDA6B2E828419CF2003E8C16 /* MeshPackets.swift */, @@ -1391,6 +1396,7 @@ children = ( DD4C11E02E8099C3003F2F2E /* PreferenceKeys */, 108FFECA2DD3F43C00BFAA81 /* ShareContactQRDialog.swift */, + DD0A10032E0292350090CE24 /* EditNodeDisplayNameView.swift */, 231B3F232D087C020069A07D /* Metrics Columns */, DDAD49EB2AFAE82500B4425D /* Map */, DDDB26432AAC0206003AFCB7 /* NodeDetail.swift */, @@ -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 */, diff --git a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift index d6d2c997..887e8bc2 100644 --- a/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/MessageEntityExtension.swift @@ -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 @@ -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 diff --git a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift index bed6d970..30a43980 100644 --- a/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/NodeInfoEntityExtension.swift @@ -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 diff --git a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift index 3a61ff4b..312c878d 100644 --- a/Meshtastic/Extensions/CoreData/UserEntityExtension.swift +++ b/Meshtastic/Extensions/CoreData/UserEntityExtension.swift @@ -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 } diff --git a/Meshtastic/Helpers/NodeDisplayNameStore.swift b/Meshtastic/Helpers/NodeDisplayNameStore.swift new file mode 100644 index 00000000..6cbbe517 --- /dev/null +++ b/Meshtastic/Helpers/NodeDisplayNameStore.swift @@ -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) + } +} diff --git a/Meshtastic/Views/Messages/UserList.swift b/Meshtastic/Views/Messages/UserList.swift index ba07a9bf..e1c5a599 100644 --- a/Meshtastic/Views/Messages/UserList.swift +++ b/Meshtastic/Views/Messages/UserList.swift @@ -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 { @@ -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() diff --git a/Meshtastic/Views/Nodes/Helpers/EditNodeDisplayNameView.swift b/Meshtastic/Views/Nodes/Helpers/EditNodeDisplayNameView.swift new file mode 100644 index 00000000..7ea54958 --- /dev/null +++ b/Meshtastic/Views/Nodes/Helpers/EditNodeDisplayNameView.swift @@ -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) ?? "" + } + } + } +} diff --git a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift index 660e57bd..28d72ef3 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeDetail.swift @@ -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 @@ -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 { @@ -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 @@ -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) } } diff --git a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift index 23a97fd0..95825208 100644 --- a/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift +++ b/Meshtastic/Views/Nodes/Helpers/NodeListItem.swift @@ -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" } @@ -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) @@ -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") diff --git a/Meshtastic/Views/Nodes/NodeList.swift b/Meshtastic/Views/Nodes/NodeList.swift index c751f84a..2d6e051f 100644 --- a/Meshtastic/Views/Nodes/NodeList.swift +++ b/Meshtastic/Views/Nodes/NodeList.swift @@ -21,6 +21,7 @@ struct NodeList: View { @State private var isPresentingDeleteNodeAlert = false @State private var deleteNodeId: Int64 = 0 @State private var shareContactNode: NodeInfoEntity? + @State private var nodeForDisplayNameEdit: NodeInfoEntity? @StateObject var filters = NodeFilterParameters() @State var isEditingFilters = false @SceneStorage("selectedDetailView") var selectedDetailView: String? @@ -40,7 +41,8 @@ struct NodeList: View { connectedNode: connectedNode, isPresentingDeleteNodeAlert: $isPresentingDeleteNodeAlert, deleteNodeId: $deleteNodeId, - shareContactNode: $shareContactNode + shareContactNode: $shareContactNode, + nodeForDisplayNameEdit: $nodeForDisplayNameEdit ) .sheet(isPresented: $isEditingFilters) { NodeListFilter( @@ -93,16 +95,19 @@ struct NodeList: View { do { try await accessoryManager.removeNode(node: node, connectedNodeNum: Int64(accessoryManager.activeDeviceNum ?? -1)) } catch { - Logger.data.error("Failed to delete node \(node.user?.longName ?? "Unknown".localized, privacy: .public)") + Logger.data.error("Failed to delete node \(node.user?.displayLongName ?? "Unknown".localized, privacy: .public)") } } } } } } - .sheet(item: $shareContactNode) { selectedNode in - ShareContactQRDialog(node: selectedNode.toProto()) - } + .sheet(item: $shareContactNode) { selectedNode in + ShareContactQRDialog(node: selectedNode.toProto()) + } + .sheet(item: $nodeForDisplayNameEdit) { node in + EditNodeDisplayNameView(node: node) + } .navigationSplitViewColumnWidth(min: 100, ideal: 300, max: .infinity) .navigationBarItems(leading: MeshtasticLogo(), trailing: ZStack { ConnectedDevice( @@ -160,6 +165,7 @@ fileprivate struct FilteredNodeList: View { @Binding var isPresentingDeleteNodeAlert: Bool @Binding var deleteNodeId: Int64 @Binding var shareContactNode: NodeInfoEntity? + @Binding var nodeForDisplayNameEdit: NodeInfoEntity? // The initializer for the FetchRequest init( @@ -168,7 +174,8 @@ fileprivate struct FilteredNodeList: View { connectedNode: NodeInfoEntity?, isPresentingDeleteNodeAlert: Binding, deleteNodeId: Binding, - shareContactNode: Binding + shareContactNode: Binding, + nodeForDisplayNameEdit: Binding ) { let request: NSFetchRequest = NodeInfoEntity.fetchRequest() request.sortDescriptors = [ @@ -185,6 +192,7 @@ fileprivate struct FilteredNodeList: View { self._isPresentingDeleteNodeAlert = isPresentingDeleteNodeAlert self._deleteNodeId = deleteNodeId self._shareContactNode = shareContactNode + self._nodeForDisplayNameEdit = nodeForDisplayNameEdit } // The body of the view @@ -213,6 +221,11 @@ fileprivate struct FilteredNodeList: View { node: NodeInfoEntity, connectedNode: NodeInfoEntity? ) -> some View { + Button { + nodeForDisplayNameEdit = node + } label: { + Label("Set display name", systemImage: "pencil.circle") + } if let user = node.user { NodeAlertsButton(context: context, node: node, user: user) if !user.unmessagable && user.num == UserDefaults.preferredPeripheralNum { diff --git a/Meshtastic/Views/Nodes/NodeRow.swift b/Meshtastic/Views/Nodes/NodeRow.swift index 91d70de8..bc656dd2 100644 --- a/Meshtastic/Views/Nodes/NodeRow.swift +++ b/Meshtastic/Views/Nodes/NodeRow.swift @@ -9,16 +9,16 @@ struct NodeRow: View { HStack { - CircleText(text: node.user?.shortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5) + CircleText(text: node.user?.displayShortName ?? "???", color: Color.accentColor).offset(y: 1).padding(.trailing, 5) .offset(x: -15) - if UIDevice.current.userInterfaceIdiom == .pad { - Text(node.user?.longName ?? "Unknown").font(.headline) - .offset(x: -15) - } else { - Text(node.user?.longName ?? "Unknown").font(.title) - .offset(x: -15) - } + if UIDevice.current.userInterfaceIdiom == .pad { + Text(node.user?.displayLongName ?? "Unknown").font(.headline) + .offset(x: -15) + } else { + Text(node.user?.displayLongName ?? "Unknown").font(.title) + .offset(x: -15) + } } .padding(.bottom, 10)