-
Notifications
You must be signed in to change notification settings - Fork 705
Nickname Collision Handling and Mention Coloring #638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -47,7 +47,7 @@ class BluetoothMeshService(private val context: Context) { | |
|
|
||
| // My peer identification - derived from persisted Noise identity fingerprint (first 16 hex chars) | ||
| val myPeerID: String = encryptionService.getIdentityFingerprint().take(16) | ||
| private val peerManager = PeerManager() | ||
| private val peerManager = PeerManager().apply { myPeerID = [email protected] } | ||
| private val fragmentManager = FragmentManager() | ||
| private val securityManager = SecurityManager(encryptionService, myPeerID) | ||
| private val storeForwardManager = StoreForwardManager() | ||
|
|
@@ -636,6 +636,10 @@ class BluetoothMeshService(private val context: Context) { | |
| if (connectionManager.startServices()) { | ||
| isActive = true | ||
|
|
||
| // 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) | ||
|
|
||
| // Start periodic announcements for peer discovery and connectivity | ||
| sendPeriodicBroadcastAnnounce() | ||
| Log.d(TAG, "Started periodic broadcast announcements (every 30 seconds)") | ||
|
|
@@ -697,6 +701,14 @@ class BluetoothMeshService(private val context: Context) { | |
| return reusable | ||
| } | ||
|
|
||
| /** | ||
| * Update our own nickname in peer manager and announce it | ||
| */ | ||
| fun updateSelfNickname(newNickname: String) { | ||
| peerManager.addOrUpdatePeer(myPeerID, newNickname) | ||
| sendBroadcastAnnounce() | ||
| } | ||
|
|
||
| /** | ||
| * Send public message | ||
| */ | ||
|
|
@@ -1175,6 +1187,11 @@ class BluetoothMeshService(private val context: Context) { | |
| * Get peer nicknames | ||
| */ | ||
| fun getPeerNicknames(): Map<String, String> = peerManager.getAllPeerNicknames() | ||
|
|
||
| /** | ||
| * Get disambiguated peer nickname (nickname#suffix if collisions exist) | ||
| */ | ||
| fun getDisambiguatedNickname(peerID: String): String = peerManager.getDisambiguatedNickname(peerID) | ||
|
|
||
| /** | ||
| * Get peer RSSI values | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,9 @@ class PeerManager { | |
| // Callback to check if a peer is directly connected (injected by BluetoothMeshService) | ||
| var isPeerDirectlyConnected: ((String) -> Boolean)? = null | ||
|
|
||
| // My own Peer ID (to treat specially in disambiguation and cleanup) | ||
| var myPeerID: String? = null | ||
|
|
||
| // Coroutines | ||
| private val managerScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) | ||
|
|
||
|
|
@@ -207,22 +210,7 @@ class PeerManager { | |
| fun addOrUpdatePeer(peerID: String, nickname: String): Boolean { | ||
| if (peerID == "unknown") return false | ||
|
|
||
| // Clean up stale peer IDs with the same nickname (exact same logic as iOS) | ||
| val now = System.currentTimeMillis() | ||
| val stalePeerIDs = mutableListOf<String>() | ||
| peers.forEach { (existingPeerID, info) -> | ||
| if (info.nickname == nickname && existingPeerID != peerID) { | ||
| val wasRecentlySeen = (now - info.lastSeen) < 10000 | ||
| if (!wasRecentlySeen) { | ||
| stalePeerIDs.add(existingPeerID) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Remove stale peer IDs | ||
| stalePeerIDs.forEach { stalePeerID -> | ||
| removePeer(stalePeerID, notifyDelegate = false) | ||
| } | ||
|
|
||
| // Check if this is a new peer announcement | ||
| val isFirstAnnounce = !announcedPeers.contains(peerID) | ||
|
|
@@ -313,6 +301,18 @@ class PeerManager { | |
| return peers[peerID]?.nickname | ||
| } | ||
|
|
||
| /** | ||
| * Get disambiguated peer nickname (nickname#suffix if collisions exist) | ||
| */ | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be made more efficient, every time a peer joins/leaves, all visible messages recompose and each one calls: Assuming we have 50 peers: Without cache:
With cache:
|
||
|
|
||
| // Suffix is appended if ambiguous, UNLESS this is our own Peer ID | ||
| return if (isAmbiguous && peerID != myPeerID) "$nick#${peerID.takeLast(4)}" else nick | ||
| } | ||
|
|
||
| /** | ||
| * Get all peer nicknames | ||
| */ | ||
|
|
@@ -433,7 +433,9 @@ class PeerManager { | |
| private fun cleanupStalePeers() { | ||
| val now = System.currentTimeMillis() | ||
|
|
||
| val peersToRemove = peers.filterValues { (now - it.lastSeen) > stalePeerTimeoutMs } | ||
| val peersToRemove = peers.filterValues { | ||
| it.id != myPeerID && (now - it.lastSeen) > stalePeerTimeoutMs | ||
| } | ||
| .keys | ||
| .toList() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,13 +130,15 @@ fun ChatScreen(viewModel: ChatViewModel) { | |
| ) | ||
|
|
||
| // Messages area - takes up available space, will compress when keyboard appears | ||
| val peerNicknames by viewModel.peerNicknames.collectAsStateWithLifecycle() | ||
| MessagesList( | ||
| messages = displayMessages, | ||
| currentUserNickname = nickname, | ||
| meshService = viewModel.meshService, | ||
| modifier = Modifier.weight(1f), | ||
| forceScrollToBottom = forceScrollToBottom, | ||
| onScrolledUpChanged = { isUp -> isScrolledUp = isUp }, | ||
| peerNicknames = peerNicknames, | ||
| onNicknameClick = { fullSenderName -> | ||
| // Single click - mention user in text input | ||
| val currentText = messageText.text | ||
|
|
@@ -167,9 +169,13 @@ fun ChatScreen(viewModel: ChatViewModel) { | |
| }, | ||
| 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 | ||
|
Comment on lines
170
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 **/ }
}
} |
||
| } | ||
| selectedUserForSheet = disambiguated | ||
| selectedMessageForSheet = message | ||
| showUserSheet = true | ||
| }, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding the local peer to
PeerManageron startup means the peer list is never empty while the service is active.MeshDelegateHandler.didUpdatePeerListsetsisConnectedbased onpeers.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,/whooutput, 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 }