This commit is contained in:
callebtc 2026-07-27 15:27:15 +02:00
parent dabc520090
commit 4863381dbb
3 changed files with 113 additions and 16 deletions

View File

@ -3,6 +3,7 @@ package com.bitchat.android.ui
import com.bitchat.android.mesh.MeshService
import com.bitchat.android.model.BitchatMessage
import java.util.Date
import java.util.Locale
/**
* Handles processing of IRC-style commands
@ -448,12 +449,13 @@ class CommandProcessor(
is com.bitchat.android.geohash.ChannelID.Mesh,
null -> {
// Mesh channel: use Bluetooth mesh peer nicknames
meshService.getPeerNicknames().values.filter { it != meshService.getPeerNicknames()[meshService.myPeerID] }
val peerNicknames = meshService.getPeerNicknames()
peerNicknames.values.filter { it != peerNicknames[meshService.myPeerID] }
}
is com.bitchat.android.geohash.ChannelID.Location -> {
// Location channel: use geohash participants with collision-resistant suffixes
val geohashPeople = viewModel.geohashPeople.value ?: emptyList()
val geohashPeople = viewModel.geohashPeople.value
val currentNickname = state.getNicknameValue()
geohashPeople.mapNotNull { person ->
@ -469,13 +471,11 @@ class CommandProcessor(
}
} else {
// Fallback to mesh peers if no viewModel available
meshService.getPeerNicknames().values.filter { it != meshService.getPeerNicknames()[meshService.myPeerID] }
val peerNicknames = meshService.getPeerNicknames()
peerNicknames.values.filter { it != peerNicknames[meshService.myPeerID] }
}
// Filter nicknames based on the text after @
val filteredNicknames = peerCandidates.filter { nickname ->
nickname.startsWith(textAfterAt, ignoreCase = true)
}.sorted()
val filteredNicknames = filterMentionCandidates(peerCandidates, textAfterAt)
if (filteredNicknames.isNotEmpty()) {
state.setMentionSuggestions(filteredNicknames)
@ -533,3 +533,27 @@ class CommandProcessor(
}
}
}
/**
* Keep mention autocomplete useful in crowded channels: a bare `anon` identity has not announced
* a username and is not actionable. Names such as `anon1234` are announced usernames and remain
* valid mention targets.
*/
internal fun filterMentionCandidates(
candidates: List<String>,
query: String
): List<String> {
return candidates.asSequence()
.map(String::trim)
.filter(String::isNotEmpty)
.filterNot(::isUnannouncedMentionNickname)
.filter { nickname -> nickname.startsWith(query, ignoreCase = true) }
.distinctBy { nickname -> nickname.lowercase(Locale.ROOT) }
.sortedWith(String.CASE_INSENSITIVE_ORDER)
.toList()
}
internal fun isUnannouncedMentionNickname(displayName: String): Boolean {
val base = splitSuffix(displayName.trim()).first
return base.equals("anon", ignoreCase = true)
}

View File

@ -1,6 +1,5 @@
package com.bitchat.android.ui
import com.bitchat.android.ui.theme.BitchatFontFamily
import com.bitchat.android.ui.theme.BitchatFontFamily
// [Goose] TODO: Replace inline file attachment stub with FilePickerButton abstraction that dispatches via FileShareDispatcher
@ -28,6 +27,8 @@ import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
@ -707,13 +708,18 @@ fun MentionSuggestionsBox(
) {
val palette = LocalBitchatPalette.current
Column(
LazyColumn(
modifier = modifier
.heightIn(max = MentionSuggestionsMaxHeight)
.clip(MentionSuggestionsShape)
.background(palette.surface)
.border(1.dp, palette.outlineVariant, RoundedCornerShape(8.dp))
.padding(vertical = 6.dp)
.border(1.dp, palette.outlineVariant, MentionSuggestionsShape),
contentPadding = PaddingValues(vertical = MentionSuggestionsVerticalPadding)
) {
suggestions.forEach { suggestion: String ->
items(
items = suggestions,
key = { suggestion -> suggestion.lowercase() }
) { suggestion ->
MentionSuggestionItem(
suggestion = suggestion,
onClick = { onSuggestionClick(suggestion) }
@ -732,8 +738,9 @@ fun MentionSuggestionItem(
Row(
modifier = Modifier
.fillMaxWidth()
.height(MentionSuggestionRowHeight)
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
@ -743,10 +750,13 @@ fun MentionSuggestionItem(
fontWeight = FontWeight.SemiBold
),
color = palette.accentOrange,
fontSize = (BASE_FONT_SIZE - 2).sp
fontSize = (BASE_FONT_SIZE - 2).sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(R.string.mention),
@ -754,7 +764,16 @@ fun MentionSuggestionItem(
fontFamily = BitchatFontFamily
),
color = palette.textTertiary,
fontSize = (BASE_FONT_SIZE - 4).sp
fontSize = (BASE_FONT_SIZE - 4).sp,
maxLines = 1
)
}
}
/** Mention autocomplete stays compact even in crowded channels. */
internal const val MaxVisibleMentionSuggestions = 5
private val MentionSuggestionRowHeight = 48.dp
private val MentionSuggestionsVerticalPadding = 6.dp
private val MentionSuggestionsMaxHeight =
(48 * MaxVisibleMentionSuggestions + 12).dp
private val MentionSuggestionsShape = RoundedCornerShape(8.dp)

View File

@ -0,0 +1,54 @@
package com.bitchat.android.ui
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class MentionSuggestionsTest {
@Test
fun `only users without an announced nickname are excluded from mentions`() {
val suggestions = filterMentionCandidates(
candidates = listOf(
"anon",
"anon#04af",
"anon7674#df5b",
"alice#1234",
"anonymous",
"anonracer#04af"
),
query = ""
)
assertEquals(
listOf("alice#1234", "anon7674#df5b", "anonracer#04af", "anonymous"),
suggestions
)
assertTrue(suggestions.none(::isUnannouncedMentionNickname))
}
@Test
fun `mention filtering is case insensitive and removes duplicates`() {
val suggestions = filterMentionCandidates(
candidates = listOf("Bob#1234", "bob#1234", "bobby#5678", "alice#9999"),
query = "BO"
)
assertEquals(listOf("Bob#1234", "bobby#5678"), suggestions)
}
@Test
fun `announced names beginning with anon stay mentionable`() {
assertTrue(isUnannouncedMentionNickname("anon"))
assertTrue(isUnannouncedMentionNickname("anon#04af"))
assertFalse(isUnannouncedMentionNickname("anon1234#04af"))
assertFalse(isUnannouncedMentionNickname("anonymous#04af"))
assertFalse(isUnannouncedMentionNickname("anonracer"))
}
@Test
fun `mention popup viewport is capped at five rows`() {
assertEquals(5, MaxVisibleMentionSuggestions)
}
}