Repository navigation
feat(nodes): local display names - #7763
Conversation
A per-node name that stays on the device, preserved across NodeInfo upserts, renumbers and database merges. Adds the Node resolver and search match.
… row Edit from the node details card or the node list menu. Node details shows the broadcast long name in its own row while a display name is set.
…s and widget Share, export, QR and CoT keep the broadcast long name.
…st name The alpha node sort and the reaction details list used the broadcast name while the rest of the UI shows the display name. A test name with a comma failed Kotlin/Native compilation.
Kotlin/Native rejects commas in backtick test names.
…urfaces The GPX track name now carries the broadcast long name. The contact list recomputes when a display name changes. Connections, settings, traceroute hops, neighbor info and the AI assistant identity use the display name. Display name limit docs no longer claim to match User.long_name.
Long press on our node now offers Update status, Edit name (opens the device User settings) and Share contact, each only when available.
Walkthrough
Priority: Merge Risk:
|
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments: 2
core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt: 1
134-149:Trivial · Functional Correctness · Quick win Make the alpha-sort test set
longNameexplicitly.The
entityhelper does not passlongName, so thelong_namecolumn keeps theNodeEntitydefault ofnull. For nodes 2 and 3, the alpha sort key then evaluates toNULL. Their order does not depend on "Bravo" or "Charlie". A brokenELSE long_namebranch would produce the same result, so the test cannot detect it.Pass the long name explicitly, and use names whose alphabetical order differs from the node number order:
🧪 Suggested fix
fun entity(num: Int, longName: String, displayName: String = "") = NodeEntity( num = num, + longName = longName, user = User.Builder() .also { wb -> wb.id = "!$num" wb.long_name = longName wb.short_name = "n$num" } .build(), displayName = displayName, ) - dao.putAll(listOf(entity(1, "Alpha", displayName = "Zulu"), entity(2, "Bravo"), entity(3, "Charlie"))) + dao.putAll(listOf(entity(1, "Alpha", displayName = "Zulu"), entity(2, "Charlie"), entity(3, "Bravo"))) val order = dao.getNodes("alpha", true, false, -1).first().map { it.node.num } - assertEquals(listOf(2, 3, 1), order) + assertEquals(listOf(3, 2, 1), order)core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseMerger.kt: 1
136-137:Trivial · Performance & Scalability · Low value Reuse the destination node snapshot.
The merge runs once per newly associated secondary transport, so this is not a material hot-path regression. However, the PR expands the existing notes-only single-row lookup to display-name nodes. Reuse the already loaded snapshot to remove this N+1 query pattern.
♻️ Suggested change
- val destNums = dest.nodeInfoDao().getAllNodesSnapshot().mapTo(mutableSetOf()) { it.num } + val destByNum = dest.nodeInfoDao().getAllNodesSnapshot().associateBy { it.num } source.nodeInfoDao().getAllNodesSnapshot().forEach { node -> + val existing = destByNum[node.num] when { - node.num !in destNums -> dest.nodeInfoDao().upsert(node) + existing == null -> dest.nodeInfoDao().upsert(node) node.notes.isNotBlank() || node.displayName.isNotBlank() -> { - val existing = dest.nodeInfoDao().getNodeByNum(node.num)?.node - if (existing != null) { - if (node.notes.isNotBlank() && existing.notes.isBlank()) { - dest.nodeInfoDao().setNodeNotes(node.num, node.notes) - } - if (node.displayName.isNotBlank() && existing.displayName.isBlank()) { - dest.nodeInfoDao().setNodeDisplayName(node.num, node.displayName) - } + if (node.notes.isNotBlank() && existing.notes.isBlank()) { + dest.nodeInfoDao().setNodeNotes(node.num, node.notes) + } + if (node.displayName.isNotBlank() && existing.displayName.isBlank()) { + dest.nodeInfoDao().setNodeDisplayName(node.num, node.displayName) } } }

Review info
-
Run configuration-
- Source mode: Auto
- Repository: meshtastic/Meshtastic-Android
- Branch: feat/local-display-names
- File: .coderabbit.yaml
- Commit: 720cb5e
- Layers: Repository YAML
- Review profile: CHILL
- Plan: Advanced
- Run ID:
5665d1af-73dc-41ba-8414-977119883eb1
Files selected for processing: 98-
.skills/compose-ui/strings-index.txtandroidApp/src/google/kotlin/org/meshtastic/app/ai/GeminiNanoDocAssistant.ktandroidApp/src/google/kotlin/org/meshtastic/app/map/MapView.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/AiFunctionProviderImpl.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolver.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/NodeInfoWriteDataSource.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/datasource/SwitchingNodeInfoWriteDataSource.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/GeofenceMonitor.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshDataHandlerImpl.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImpl.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/TracerouteHandlerImpl.ktcore/data/src/commonMain/kotlin/org/meshtastic/core/data/repository/NodeRepositoryImpl.ktcore/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/NeighborInfoHandlerImplTest.ktcore/database/schemas/org.meshtastic.core.database.MeshtasticDatabase/66.jsoncore/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseBackup.ktcore/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseMerger.ktcore/database/src/commonMain/kotlin/org/meshtastic/core/database/MeshtasticDatabase.ktcore/database/src/commonMain/kotlin/org/meshtastic/core/database/dao/NodeInfoDao.ktcore/database/src/commonMain/kotlin/org/meshtastic/core/database/entity/NodeEntity.ktcore/database/src/commonTest/kotlin/org/meshtastic/core/database/DatabaseMergerTest.ktcore/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeIdentityMigrationDaoTest.ktcore/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.ktcore/database/src/commonTest/kotlin/org/meshtastic/core/database/entity/NodeMappingTest.ktcore/database/src/jvmTest/kotlin/org/meshtastic/core/database/NodeDisplayNameMigrationTest.ktcore/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.ktcore/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/settings/ExportNodeDatabaseUseCaseTest.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/HamName.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/Node.ktcore/model/src/commonMain/kotlin/org/meshtastic/core/model/RouteDiscovery.ktcore/model/src/commonTest/kotlin/org/meshtastic/core/model/NodeDisplayNameTest.ktcore/model/src/commonTest/kotlin/org/meshtastic/core/model/RouteDiscoveryTest.ktcore/model/src/commonTest/kotlin/org/meshtastic/core/model/util/ToSharedContactTest.ktcore/repository/src/commonMain/kotlin/org/meshtastic/core/repository/NodeRepository.ktcore/resources/src/commonMain/composeResources/values/strings.xmlcore/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.ktcore/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/MeshNodeCoTConversionTest.ktcore/takserver/src/commonTest/kotlin/org/meshtastic/core/takserver/TAKMeshIntegrationTest.ktcore/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeNodeRepository.ktcore/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeNodeRepositoryTest.ktcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItem.ktcore/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/NodeItemCompact.ktdesktopApp/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshNotificationManager.ktdocs/en/user/nodes.mddocs/en/user/settings-radio-user.mdfeature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/LiveUpdateViewModel.ktfeature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.ktfeature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/DeviceListItem.ktfeature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/LiveUpdateCard.ktfeature/map-maplibre/src/commonMain/kotlin/org/meshtastic/feature/map/maplibre/geojson/NodeFeatures.ktfeature/map/src/commonMain/kotlin/org/meshtastic/feature/map/component/SitePlannerLaunch.ktfeature/map/src/commonMain/kotlin/org/meshtastic/feature/map/hud/HudChips.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/Message.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageListPaged.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/MessageViewModel.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/Reaction.ktfeature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModel.ktfeature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/compass/CompassViewModel.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/AdministrationSection.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DisplayNameDialog.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DisplayNameSection.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/LinkedCoordinatesItem.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeContextMenu.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/NodeDetailsSection.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailContent.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailPreviews.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailScreens.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeDetailViewModel.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/detail/NodeManagementActions.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/domain/usecase/CommonGetNodeDetailsUseCase.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListScreen.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/list/NodeListViewModel.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/AirQualityMetrics.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/DeviceMetrics.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/EnvironmentMetrics.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/HostMetricsLog.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/NeighborInfoLog.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PaxMetrics.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PositionLogScreens.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/PowerMetrics.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/SignalMetrics.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteLog.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/TracerouteMapScreen.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/metrics/terminal/RemoteShellViewModel.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/navigation/AdaptiveNodeListScreen.ktfeature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.ktfeature/node/src/commonTest/kotlin/org/meshtastic/feature/node/domain/usecase/NodeDetailNameUseCaseTest.ktfeature/node/src/commonTest/kotlin/org/meshtastic/feature/node/list/OwnNodeMenuItemsTest.ktfeature/node/src/commonTest/kotlin/org/meshtastic/feature/node/metrics/GpxTrackNameTest.ktfeature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/SettingsScreen.ktfeature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/component/RadioAdminAppBar.ktfeature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/radio/component/ShutdownConfirmationDialog.ktfeature/widget/src/main/kotlin/org/meshtastic/feature/widget/NodeTrackerCandidates.ktfeature/widget/src/main/kotlin/org/meshtastic/feature/widget/NodeTrackerConfigActivity.ktfeature/widget/src/main/kotlin/org/meshtastic/feature/widget/NodeTrackerWidgetState.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.
-
- AI recipient lookup treats an exact name shared by two nodes as ambiguous - DM shortcuts republish when only a display name changes - Open-in-maps passes the broadcast name to the external app - Name fields trim leading whitespace before the byte cap; the node details draft is keyed by node - User docs cover backups, the maps handoff and own-node Edit name
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

Minor · Keep the broadcast long name resolvable. · FuzzyNameResolver.kt:43
core/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolver.kt:43
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winKeep the broadcast long name resolvable.
When a node has a different local display name,
resolveNodeNameremoves its broadcast long name from the candidate list. A query using the broadcast long name can now returnNotFound, although that query resolved before this change. Include both names as aliases for the same node. Deduplicate matches by node number so aliases do not create false ambiguity. As per path instructions, report “behaviour that contradicts ... an existing contract.”Source: Path instructions

Review info
-
Run configuration-
- Source mode: Auto
- Repository: meshtastic/Meshtastic-Android
- Branch: feat/local-display-names
- File: .coderabbit.yaml
- Commit: 307a98c
- Layers: Repository YAML
- Review profile: CHILL
- Plan: Advanced
- Run ID:
a3c31704-8d30-40c0-a348-4e180f2ec88b
Files selected for processing: 7-
core/data/src/commonMain/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolver.ktcore/data/src/commonTest/kotlin/org/meshtastic/core/data/ai/FuzzyNameResolverTest.ktcore/service/src/androidHostTest/kotlin/org/meshtastic/core/service/ConversationShortcutPublisherTest.ktcore/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.ktdocs/en/user/nodes.mdfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DisplayNameDialog.ktfeature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DisplayNameSection.kt
Files skipped from review as they are similar to previous changes: 4-
- core/service/src/androidMain/kotlin/org/meshtastic/core/service/ConversationShortcutPublisher.kt
- feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DisplayNameDialog.kt
- docs/en/user/nodes.md
- feature/node/src/commonMain/kotlin/org/meshtastic/feature/node/component/DisplayNameSection.kt
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.
-
Brings Android in line with design#170 - people can give any node a local display name, and node details keeps the broadcast long name readable while one is set. Apple already ships this; Notes stays as is.
🌟 New Features
display_namecolumn onnodes(Room 65 → 66 auto-migration), preserved across NodeInfo upserts, identity renumbers and DB mergesNode.effectiveLongNameis the one resolver for display; share, QR, contact sharing, exports (messages, node DB, GPX, debug log), open-in-maps, the TAK callsign and the avatar stay onuser.long_name🧹 Chores
Node.ktandAirQualityMetrics.ktcarry a few reflow-only hunks - spotless reformats the whole file once it's touchedTesting Performed
NodeDisplayNameMigrationTestCommonNodeInfoDaoTest(upsert preservation, alpha sort),CommonNodeIdentityMigrationDaoTest,DatabaseMergerTest,NodeMappingTest,FakeNodeRepositoryTestNodeDisplayNameTest,NodeDetailNameUseCaseTest,RouteDiscoveryTest,NeighborInfoHandlerImplTest,ContactsViewModelTest,ConversationShortcutPublisherTest,FuzzyNameResolverTestExportDataUseCaseTest,ExportNodeDatabaseUseCaseTest,GpxTrackNameTest,ToSharedContactTest,MeshNodeCoTConversionTest,TAKMeshIntegrationTestNodeManagementActionsTest,OwnNodeMenuItemsTestFixes #7760
Summary by CodeRabbit