Nickname Collision Handling and Mention Coloring#638
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f4dced3399
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // Add ourselves to peer manager for collision detection | ||
| val myNickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID } | ||
| peerManager.addOrUpdatePeer(myPeerID, myNickname) |
There was a problem hiding this comment.
Don’t mark mesh connected when only self is present
Adding the local peer to PeerManager on startup means the peer list is never empty while the service is active. MeshDelegateHandler.didUpdatePeerList sets isConnected based on peers.isNotEmpty(), so with this change the app reports a connected mesh even when no other devices are around. That breaks UI/logic that assumes “connected” implies at least one remote peer (status indicators, /who output, etc.). Consider keeping self out of the active peer list or filtering it before computing connection state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This also causes issues like being able to open a chat with yourself, this is best solved at data layer level by filtering getActivePeerIDs() in PeerManager.kt to exclude self. This will solve it for isConnected and all other cases.
return peers.filterValues { it.isConnected && it.id != myPeerID }
/**
* Get list of active peer IDs
*/
fun getActivePeerIDs(): List<String> {
return peers.filterValues { it.isConnected && it.id != myPeerID }
.keys
.toList()
.sorted()
}
moehamade
left a comment
There was a problem hiding this comment.
When using /mention , it correctly shows the ambiguous users in the commands, but when using any other command like /m or /block etc.. I think we should do the same in that case.
Critical fixes would be to filter self from active peers and double check displayed hashes if it's similar with iOS.
Medium improvements would be to fix ambiguous names caching with using something like:
private val ambiguousNicknamesCache = ConcurrentHashMap.newKeySet<String>()
Rest are optional improvements but nice to be done.
| // Add ourselves to peer manager for collision detection | ||
| val myNickname = try { com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) } catch (_: Exception) { myPeerID } | ||
| peerManager.addOrUpdatePeer(myPeerID, myNickname) |
There was a problem hiding this comment.
This also causes issues like being able to open a chat with yourself, this is best solved at data layer level by filtering getActivePeerIDs() in PeerManager.kt to exclude self. This will solve it for isConnected and all other cases.
return peers.filterValues { it.isConnected && it.id != myPeerID }
/**
* Get list of active peer IDs
*/
fun getActivePeerIDs(): List<String> {
return peers.filterValues { it.isConnected && it.id != myPeerID }
.keys
.toList()
.sorted()
}| fun getDisambiguatedNickname(peerID: String): String { | ||
| val info = peers[peerID] ?: return peerID | ||
| val nick = info.nickname.trim() | ||
| val isAmbiguous = peers.values.count { it.nickname.trim().equals(nick, ignoreCase = true) } > 1 |
There was a problem hiding this comment.
I think this can be made more efficient, every time a peer joins/leaves, all visible messages recompose and each one calls:
peers.values.count { it.nickname.trim().equals(nick, ignoreCase = true) } > 1
Consider using a Set to cache ambiguous names instead of calculating this for every message.
Not really a big issue because only 20 messages are shown on the screen anyway, but it's not optimal.
Assuming we have 50 peers:
Without cache:
- 20 visible messages × 50 peers = 1,000 string comparisons per peer change
With cache:
- Rebuild cache: 50 iterations (once)
- Lookups: 20 Set checks (O(1) each)
- Total: 70 operations
- Savings: 93%
| onMessageLongPress = { message -> | ||
| // Message long press - open user action sheet with message context | ||
| // Extract base nickname from message sender (contains all necessary info) | ||
| val (baseName, _) = splitSuffix(message.sender) | ||
| selectedUserForSheet = baseName | ||
| // Use disambiguated name so actions like /block work with suffixes if collisions exist | ||
| val disambiguated = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) { | ||
| viewModel.meshService.getDisambiguatedNickname(message.senderPeerID) | ||
| } else { | ||
| message.sender |
There was a problem hiding this comment.
Enhancement (Optional): Compose UI should be as dumb as possible, we use viewModels as brains to control what is usually displayed on the screen, or helper functions for formatting UI stuff.
I would personally move this to a viewModel
// In ChatViewModel.kt
fun handleMessageLongPress(message: BitchatMessage) {
val disambiguated = if (message.senderPeerID != null && !message.senderPeerID.startsWith("nostr_")) {
meshService.getDisambiguatedNickname(message.senderPeerID)
} else {
message.sender
}
```
}
// In ChatScreen.kt
onMessageLongPress = viewModel::handleMessageLongPressor use an MVI (model view intent) approach that sends events/ intents from screen UI to the viewModel and proceed this event in the viewModel:
// in ChatScreen
onMessageLongPress = { onAction(MessageLongPressed(message)) }
// in ChatViewmodel
fun onAction(action: ChatActions){
when(action){
is MessageLongPressed -> { /** Do some work **/ }
}
}| val resolution = resolvePeerIDForNickname(targetName, meshService) | ||
|
|
||
| when (resolution) { | ||
| is PeerResolutionResult.Found -> { | ||
| privateChatManager.unblockPeer(resolution.peerID, meshService) | ||
| } | ||
| is PeerResolutionResult.Ambiguous -> { | ||
| val systemMessage = BitchatMessage( | ||
| sender = "system", | ||
| content = "multiple users found with nickname '$targetName'. please use one of: ${resolution.candidates.joinToString(", ")}", | ||
| timestamp = Date(), | ||
| isRelay = false | ||
| ) | ||
| messageManager.addMessage(systemMessage) | ||
| } | ||
| is PeerResolutionResult.NotFound -> { | ||
| val systemMessage = BitchatMessage( | ||
| sender = "system", | ||
| content = "user '$targetName' not found.", | ||
| timestamp = Date(), | ||
| isRelay = false | ||
| ) | ||
| messageManager.addMessage(systemMessage) | ||
| } | ||
| } |
There was a problem hiding this comment.
Noticed there's a lot of code duplication that can be extracted into a helper function (we're currently doing same checks and creating same messages), and content should be extracted to string resources for translations.
| fun resolvePeerIDFromDisplayName(displayName: String, nicknames: Map<String, String>): String? { | ||
| val (base, suffix) = splitSuffix(displayName) | ||
|
|
||
| return nicknames.entries.find { (id, nick) -> | ||
| if (suffix.isNotEmpty()) { | ||
| // Match nickname and Peer ID ending | ||
| nick.equals(base, ignoreCase = true) && id.endsWith(suffix.removePrefix("#"), ignoreCase = true) | ||
| } else { | ||
| // Match nickname only | ||
| nick.equals(displayName, ignoreCase = true) | ||
| } | ||
| }?.key | ||
| } |
There was a problem hiding this comment.
I'm not sure if it's correct,I think this is wrong, that's what I got from debugging:
base = "anon6250"
suffix = "#d116"
id = "d1169b550ebfb205"
nick = "anon6250"
displayName = "anon6250#d116"
displayName = "anon6250#d116"
should create:
"$nick#${peerID.takeLast(4)}" // Should be "anon6250#b205"
it's using:
"$nick#${peerID.take(4)}" // Created "anon6250#d116"
maybe consider using
takeLast instead of endsWith or double checking what we're using on iOS vs Android.
Open from 3 devices (1 iOS and 1 Android at least), and check the 3rd device hashes from Phone 1 and Phone 2, I think they are displaying different hashes, maybe one platform is taking first 4 and the other is taking last 4?
Summary
This PR implements comprehensive nickname collision handling and identity-aware mention coloring to prevent impersonation and improve chat clarity.
Key Changes
alicevsalice#1234).ChatUIUtilsto resolve mentions back to Peer IDs. Mentions are now rendered in the actual color of the mentioned user rather than the sender's color./msg,/block, and/unblockto detect ambiguous nicknames and prompt the user to use the unique#suffixidentifier.ClassCastExceptionin the sidebar and made the mention autocomplete deterministic.Fixes #589