diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index f40549b6..20e6544f 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -1675,6 +1675,11 @@ class ChatViewModel( geohashViewModel.blockUserInGeohash(targetNickname) } + /** True when this Nostr pubkey is blocked for geohash/location chats. */ + fun isGeohashPubkeyBlocked(pubkeyHex: String): Boolean { + return dataManager.isGeohashUserBlocked(pubkeyHex.lowercase()) + } + // MARK: - Navigation Management fun showAppInfo() { diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index 1cccfb19..b811c9c8 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -562,14 +562,20 @@ class CommandProcessor( when (val selectedChannel = viewModel.selectedLocationChannel.value) { is com.bitchat.android.geohash.ChannelID.Mesh, null -> { - // Mesh channel: use Bluetooth mesh peer nicknames - val peerNicknames = meshService.getPeerNicknames() - peerNicknames.values.filter { it != peerNicknames[meshService.myPeerID] } + meshMentionCandidates( + peerNicknames = meshService.getPeerNicknames(), + myPeerID = meshService.myPeerID.orEmpty(), + isPeerBlocked = privateChatManager::isPeerBlocked + ) } is com.bitchat.android.geohash.ChannelID.Location -> { // Location channel: use geohash participants with collision-resistant suffixes - val geohashPeople = viewModel.geohashPeople.value + val geohashPeople = excludeBlockedGeohashPeople( + people = viewModel.geohashPeople.value, + pubkeyOf = { it.id }, + isBlocked = viewModel::isGeohashPubkeyBlocked + ) val currentNickname = state.getNicknameValue() val duplicateNames = duplicateGeohashBaseNames(geohashPeople) @@ -593,9 +599,11 @@ class CommandProcessor( } } } else { - // Fallback to mesh peers if no viewModel available - val peerNicknames = meshService.getPeerNicknames() - peerNicknames.values.filter { it != peerNicknames[meshService.myPeerID] } + meshMentionCandidates( + peerNicknames = meshService.getPeerNicknames(), + myPeerID = meshService.myPeerID.orEmpty(), + isPeerBlocked = privateChatManager::isPeerBlocked + ) } val filteredNicknames = filterMentionCandidates(peerCandidates, textAfterAt) @@ -719,3 +727,30 @@ internal fun filterMentionCandidates( .sortedWith(String.CASE_INSENSITIVE_ORDER) .toList() } + +/** + * Mesh @-mention candidates excluding self and blocked peers (iOS ChatComposerCoordinator parity). + */ +internal fun meshMentionCandidates( + peerNicknames: Map, + myPeerID: String, + isPeerBlocked: (String) -> Boolean +): List { + val myNickname = peerNicknames[myPeerID] + return peerNicknames + .filter { (peerID, nick) -> + nick != myNickname && !isPeerBlocked(peerID) + } + .values + .toList() +} + +/** + * Drop geohash people whose Nostr pubkey is blocked before building mention tokens. + */ +internal fun excludeBlockedGeohashPeople( + people: List, + pubkeyOf: (T) -> String, + isBlocked: (String) -> Boolean +): List = people.filterNot { isBlocked(pubkeyOf(it)) } + diff --git a/app/src/test/java/com/bitchat/android/ui/MentionSuggestionsTest.kt b/app/src/test/java/com/bitchat/android/ui/MentionSuggestionsTest.kt index 607aaf1e..d89aaaea 100644 --- a/app/src/test/java/com/bitchat/android/ui/MentionSuggestionsTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/MentionSuggestionsTest.kt @@ -51,4 +51,34 @@ class MentionSuggestionsTest { fun `mention popup viewport is capped at five rows`() { assertEquals(5, MaxVisibleMentionSuggestions) } + + @Test + fun `mesh mention candidates exclude self and blocked peers`() { + val nicknames = mapOf( + "aaaa" to "me", + "bbbb" to "alice", + "cccc" to "eve" + ) + val candidates = meshMentionCandidates( + peerNicknames = nicknames, + myPeerID = "aaaa", + isPeerBlocked = { it == "cccc" } + ) + assertEquals(listOf("alice"), candidates) + } + + @Test + fun `geohash mention candidates exclude blocked pubkeys`() { + data class Person(val id: String, val name: String) + val people = listOf( + Person("aaaabbbb", "carol"), + Person("bbbbcccc", "blocked") + ) + val kept = excludeBlockedGeohashPeople( + people = people, + pubkeyOf = { it.id }, + isBlocked = { it == "bbbbcccc" } + ) + assertEquals(listOf("carol"), kept.map { it.name }) + } }