fix: show the same #xxxx mesh suffix the nickname resolver accepts

The people list computed a collision but never rendered a suffix, so /msg alice refused while alice#1111 was not on the row.
This commit is contained in:
Taksh 2026-08-25 08:27:34 +05:30
parent 2f72d1a07c
commit b9dd5bfe6e
5 changed files with 75 additions and 6 deletions

View File

@ -241,9 +241,11 @@ fun ChatScreen(viewModel: ChatViewModel) {
displayName to PeerIdentity.nostr(person.id)
}
} else {
val duplicateNames = duplicateMeshBaseNames(peerNicknames)
connectedPeers.mapNotNull { peerID ->
peerNicknames[peerID]?.let { displayName ->
displayName to PeerIdentity.mesh(peerID)
disambiguatedMeshDisplayName(peerID, displayName, duplicateNames) to
PeerIdentity.mesh(peerID)
}
}
}

View File

@ -390,6 +390,42 @@ internal fun uniquePeerIDForNickname(
suffixOf: (String) -> String = { "#${it.take(4)}" },
): String? = (resolvePeerIDForNickname(query, nicknames, suffixOf) as? NicknameResolution.Unique)?.peerID
/** First four hex characters of a mesh peer ID, matching mention coloring. */
internal fun meshPeerSuffix(peerID: String): String = "#${peerID.take(4)}"
internal fun duplicateMeshBaseNames(nicknames: Map<String, String>): Set<String> =
nicknames.values
.groupingBy { splitSuffix(it).first.lowercase(Locale.ROOT) }
.eachCount()
.filterValues { it > 1 }
.keys
/**
* People-list / autocomplete suffix for a mesh peer.
*
* Mesh nicknames are stored unsuffixed, unlike some geohash announcements, so a collision
* has to derive `#xxxx` from the peer ID. Preserve an already-embedded suffix if present.
*/
internal fun meshIdentitySuffix(
peerID: String,
displayName: String,
showHashSuffix: Boolean,
): String {
if (!showHashSuffix || displayName == "You") return ""
val announcedSuffix = splitSuffix(displayName).second
return announcedSuffix.ifEmpty { meshPeerSuffix(peerID) }
}
internal fun disambiguatedMeshDisplayName(
peerID: String,
displayName: String,
duplicateBaseNames: Set<String>,
): String {
val baseName = splitSuffix(displayName).first
val showSuffix = baseName.lowercase(Locale.ROOT) in duplicateBaseNames
return baseName + meshIdentitySuffix(peerID, displayName, showSuffix)
}
/**
* Build a case-insensitive mention-token lookup from canonical peer identities.
*

View File

@ -573,9 +573,15 @@ class CommandProcessor(
when (val selectedChannel = viewModel.selectedLocationChannel.value) {
is com.bitchat.android.geohash.ChannelID.Mesh,
null -> {
// Mesh channel: use Bluetooth mesh peer nicknames
// Mesh channel: use Bluetooth mesh peer nicknames, with the same
// #xxxx suffix the people list shows when two peers share a name.
val peerNicknames = meshService.getPeerNicknames()
peerNicknames.values.filter { it != peerNicknames[meshService.myPeerID] }
val myPeerID = meshService.myPeerID
val duplicateNames = duplicateMeshBaseNames(peerNicknames)
peerNicknames.mapNotNull { (peerID, name) ->
if (peerID == myPeerID) null
else disambiguatedMeshDisplayName(peerID, name, duplicateNames)
}
}
is com.bitchat.android.geohash.ChannelID.Location -> {
@ -606,7 +612,12 @@ class CommandProcessor(
} else {
// Fallback to mesh peers if no viewModel available
val peerNicknames = meshService.getPeerNicknames()
peerNicknames.values.filter { it != peerNicknames[meshService.myPeerID] }
val myPeerID = meshService.myPeerID
val duplicateNames = duplicateMeshBaseNames(peerNicknames)
peerNicknames.mapNotNull { (peerID, name) ->
if (peerID == myPeerID) null
else disambiguatedMeshDisplayName(peerID, name, duplicateNames)
}
}
val filteredNicknames = filterMentionCandidates(peerCandidates, textAfterAt)

View File

@ -1480,9 +1480,9 @@ private fun PeerItem(
isDirect = isDirect
)
// Split display name for hashtag suffix support (iOS-compatible)
val (baseNameRaw, suffixRaw) = splitSuffix(displayName)
val (baseNameRaw, _) = splitSuffix(displayName)
val baseName = truncateNickname(baseNameRaw)
val suffix = if (showHashSuffix) suffixRaw else ""
val suffix = meshIdentitySuffix(peerID, displayName, showHashSuffix)
val isMe = displayName == "You" || peerID == currentNickname
// Get consistent peer color (iOS-compatible)

View File

@ -351,6 +351,26 @@ class ChatUIUtilsTest {
)
}
@Test
fun `mesh people-list suffix is the first four hex characters of the peer id`() {
val nicknames = mapOf(
"1111aaaa" to "alice",
"2222bbbb" to "alice",
"ccccdddd" to "bob",
)
val duplicates = duplicateMeshBaseNames(nicknames)
assertEquals(setOf("alice"), duplicates)
assertEquals("alice#1111", disambiguatedMeshDisplayName("1111aaaa", "alice", duplicates))
assertEquals("alice#2222", disambiguatedMeshDisplayName("2222bbbb", "alice", duplicates))
assertEquals("bob", disambiguatedMeshDisplayName("ccccdddd", "bob", duplicates))
assertEquals(
"#1111",
meshIdentitySuffix("1111aaaa", "alice", showHashSuffix = true)
)
assertEquals("", meshIdentitySuffix("1111aaaa", "alice", showHashSuffix = false))
assertEquals("", meshIdentitySuffix("1111aaaa", "You", showHashSuffix = true))
}
@Test
fun `unknown nickname resolves to none`() {
assertEquals(