mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-22 07:06:05 +00:00
mentions
This commit is contained in:
parent
dabc520090
commit
4863381dbb
@ -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)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user