mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
user bottom sheet (#285)
* wip sheet * longpress * block wip * blocking works
This commit is contained in:
@@ -28,7 +28,8 @@ class NostrGeohashService(
|
||||
private val messageManager: MessageManager,
|
||||
private val privateChatManager: PrivateChatManager,
|
||||
private val meshDelegateHandler: MeshDelegateHandler,
|
||||
private val coroutineScope: CoroutineScope
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val dataManager: com.bitchat.android.ui.DataManager
|
||||
) {
|
||||
|
||||
companion object {
|
||||
@@ -49,7 +50,8 @@ class NostrGeohashService(
|
||||
messageManager: MessageManager,
|
||||
privateChatManager: PrivateChatManager,
|
||||
meshDelegateHandler: MeshDelegateHandler,
|
||||
coroutineScope: CoroutineScope
|
||||
coroutineScope: CoroutineScope,
|
||||
dataManager: com.bitchat.android.ui.DataManager
|
||||
): NostrGeohashService {
|
||||
return synchronized(this) {
|
||||
INSTANCE ?: NostrGeohashService(
|
||||
@@ -58,7 +60,8 @@ class NostrGeohashService(
|
||||
messageManager,
|
||||
privateChatManager,
|
||||
meshDelegateHandler,
|
||||
coroutineScope
|
||||
coroutineScope,
|
||||
dataManager
|
||||
).also { INSTANCE = it }
|
||||
}
|
||||
}
|
||||
@@ -914,6 +917,12 @@ class NostrGeohashService(
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this user is blocked in geohash channels BEFORE any processing
|
||||
if (isGeohashUserBlocked(event.pubkey)) {
|
||||
Log.v(TAG, "🚫 Skipping event from blocked geohash user: ${event.pubkey.take(8)}...")
|
||||
return
|
||||
}
|
||||
|
||||
// Deduplicate events
|
||||
if (processedNostrEvents.contains(event.id)) {
|
||||
Log.v(TAG, "❌ Skipping duplicate event ${event.id.take(8)}...")
|
||||
@@ -1263,5 +1272,50 @@ class NostrGeohashService(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Geohash Blocking
|
||||
|
||||
/**
|
||||
* Block a user in geohash channels by their nickname
|
||||
*/
|
||||
fun blockUserInGeohash(targetNickname: String) {
|
||||
// Find the pubkey for this nickname
|
||||
val pubkeyHex = geoNicknames.entries.firstOrNull { (_, nickname) ->
|
||||
val baseName = nickname.split("#").firstOrNull() ?: nickname
|
||||
baseName == targetNickname
|
||||
}?.key
|
||||
|
||||
if (pubkeyHex != null) {
|
||||
// Add to geohash block list
|
||||
dataManager.addGeohashBlockedUser(pubkeyHex)
|
||||
|
||||
// Add system message
|
||||
val systemMessage = com.bitchat.android.model.BitchatMessage(
|
||||
sender = "system",
|
||||
content = "blocked $targetNickname in geohash channels",
|
||||
timestamp = java.util.Date(),
|
||||
isRelay = false
|
||||
)
|
||||
messageManager.addMessage(systemMessage)
|
||||
|
||||
Log.i(TAG, "🚫 Blocked geohash user: $targetNickname (pubkey: ${pubkeyHex.take(8)}...)")
|
||||
} else {
|
||||
// User not found
|
||||
val systemMessage = com.bitchat.android.model.BitchatMessage(
|
||||
sender = "system",
|
||||
content = "user '$targetNickname' not found in current geohash",
|
||||
timestamp = java.util.Date(),
|
||||
isRelay = false
|
||||
)
|
||||
messageManager.addMessage(systemMessage)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is blocked in geohash channels
|
||||
*/
|
||||
private fun isGeohashUserBlocked(pubkeyHex: String): Boolean {
|
||||
return dataManager.isGeohashUserBlocked(pubkeyHex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
var showPasswordDialog by remember { mutableStateOf(false) }
|
||||
var passwordInput by remember { mutableStateOf("") }
|
||||
var showLocationChannelsSheet by remember { mutableStateOf(false) }
|
||||
var showUserSheet by remember { mutableStateOf(false) }
|
||||
var selectedUserForSheet by remember { mutableStateOf("") }
|
||||
var forceScrollToBottom by remember { mutableStateOf(false) }
|
||||
|
||||
// Show password dialog when needed
|
||||
@@ -87,7 +89,42 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
currentUserNickname = nickname,
|
||||
meshService = viewModel.meshService,
|
||||
modifier = Modifier.weight(1f),
|
||||
forceScrollToBottom = forceScrollToBottom
|
||||
forceScrollToBottom = forceScrollToBottom,
|
||||
onNicknameClick = { fullSenderName ->
|
||||
// Single click - mention user in text input
|
||||
val currentText = messageText.text
|
||||
|
||||
// Extract base nickname and hash suffix from full sender name
|
||||
val (baseName, hashSuffix) = splitSuffix(fullSenderName)
|
||||
|
||||
// Check if we're in a geohash channel to include hash suffix
|
||||
val selectedLocationChannel = viewModel.selectedLocationChannel.value
|
||||
val mentionText = if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location && hashSuffix.isNotEmpty()) {
|
||||
// In geohash chat - include the hash suffix from the full display name
|
||||
"@$baseName$hashSuffix"
|
||||
} else {
|
||||
// Regular chat - just the base nickname
|
||||
"@$baseName"
|
||||
}
|
||||
|
||||
val newText = when {
|
||||
currentText.isEmpty() -> "$mentionText "
|
||||
currentText.endsWith(" ") -> "$currentText$mentionText "
|
||||
else -> "$currentText $mentionText "
|
||||
}
|
||||
|
||||
messageText = TextFieldValue(
|
||||
text = newText,
|
||||
selection = TextRange(newText.length)
|
||||
)
|
||||
},
|
||||
onNicknameLongPress = { fullSenderName ->
|
||||
// Long press - open user action sheet
|
||||
// Extract base nickname from full sender name
|
||||
val (baseName, _) = splitSuffix(fullSenderName)
|
||||
selectedUserForSheet = baseName
|
||||
showUserSheet = true
|
||||
}
|
||||
)
|
||||
// Input area - stays at bottom
|
||||
ChatInputSection(
|
||||
@@ -205,6 +242,9 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
||||
onAppInfoDismiss = { viewModel.hideAppInfo() },
|
||||
showLocationChannelsSheet = showLocationChannelsSheet,
|
||||
onLocationChannelsSheetDismiss = { showLocationChannelsSheet = false },
|
||||
showUserSheet = showUserSheet,
|
||||
onUserSheetDismiss = { showUserSheet = false },
|
||||
selectedUserForSheet = selectedUserForSheet,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
@@ -337,6 +377,9 @@ private fun ChatDialogs(
|
||||
onAppInfoDismiss: () -> Unit,
|
||||
showLocationChannelsSheet: Boolean,
|
||||
onLocationChannelsSheetDismiss: () -> Unit,
|
||||
showUserSheet: Boolean,
|
||||
onUserSheetDismiss: () -> Unit,
|
||||
selectedUserForSheet: String,
|
||||
viewModel: ChatViewModel
|
||||
) {
|
||||
// Password dialog
|
||||
@@ -363,4 +406,14 @@ private fun ChatDialogs(
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
// User action sheet
|
||||
if (showUserSheet) {
|
||||
ChatUserSheet(
|
||||
isPresented = showUserSheet,
|
||||
onDismiss = onUserSheetDismiss,
|
||||
targetNickname = selectedUserForSheet,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,13 +69,25 @@ fun formatMessageAsAnnotatedString(
|
||||
builder.append("<@")
|
||||
builder.pop()
|
||||
|
||||
// Base name
|
||||
// Base name (clickable)
|
||||
builder.pushStyle(SpanStyle(
|
||||
color = baseColor,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
|
||||
))
|
||||
val nicknameStart = builder.length
|
||||
builder.append(baseName)
|
||||
val nicknameEnd = builder.length
|
||||
|
||||
// Add click annotation for nickname (store full sender name with hash)
|
||||
if (!isSelf) {
|
||||
builder.addStringAnnotation(
|
||||
tag = "nickname_click",
|
||||
annotation = message.sender, // Store full sender name with hash
|
||||
start = nicknameStart,
|
||||
end = nicknameEnd
|
||||
)
|
||||
}
|
||||
builder.pop()
|
||||
|
||||
// Hashtag suffix in lighter color (iOS style)
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* User Action Sheet for selecting actions on a specific user (slap, hug, block)
|
||||
* Design language matches LocationChannelsSheet.kt for consistency
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatUserSheet(
|
||||
isPresented: Boolean,
|
||||
onDismiss: () -> Unit,
|
||||
targetNickname: String,
|
||||
viewModel: ChatViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
// Bottom sheet state
|
||||
val sheetState = rememberModalBottomSheetState(
|
||||
skipPartiallyExpanded = true
|
||||
)
|
||||
|
||||
// iOS system colors (matches LocationChannelsSheet exactly)
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
|
||||
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green
|
||||
val standardBlue = Color(0xFF007AFF) // iOS blue
|
||||
val standardRed = Color(0xFFFF3B30) // iOS red
|
||||
|
||||
if (isPresented) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState,
|
||||
modifier = modifier
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
// Header
|
||||
Text(
|
||||
text = "@$targetNickname",
|
||||
fontSize = 18.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "choose an action for this user",
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
|
||||
)
|
||||
|
||||
// Action list (iOS-style plain list)
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Slap action
|
||||
item {
|
||||
UserActionRow(
|
||||
title = "slap $targetNickname",
|
||||
subtitle = "send a playful slap message",
|
||||
titleColor = standardBlue,
|
||||
onClick = {
|
||||
// Send slap command
|
||||
viewModel.sendMessage("/slap $targetNickname")
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Hug action
|
||||
item {
|
||||
UserActionRow(
|
||||
title = "hug $targetNickname",
|
||||
subtitle = "send a friendly hug message",
|
||||
titleColor = standardGreen,
|
||||
onClick = {
|
||||
// Send hug command
|
||||
viewModel.sendMessage("/hug $targetNickname")
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Block action
|
||||
item {
|
||||
UserActionRow(
|
||||
title = "block $targetNickname",
|
||||
subtitle = "block all messages from this user",
|
||||
titleColor = standardRed,
|
||||
onClick = {
|
||||
// Check if we're in a geohash channel
|
||||
val selectedLocationChannel = viewModel.selectedLocationChannel.value
|
||||
if (selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location) {
|
||||
// Get user's nostr public key and add to geohash block list
|
||||
viewModel.blockUserInGeohash(targetNickname)
|
||||
} else {
|
||||
// Regular mesh blocking
|
||||
viewModel.sendMessage("/block $targetNickname")
|
||||
}
|
||||
onDismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel button (iOS-style)
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f),
|
||||
contentColor = MaterialTheme.colorScheme.onSurface
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = "cancel",
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UserActionRow(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
titleColor: Color,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
// iOS-style list row (plain button, no card background)
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
color = Color.Transparent,
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 14.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = titleColor
|
||||
)
|
||||
|
||||
Text(
|
||||
text = subtitle,
|
||||
fontSize = 12.sp,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,8 @@ class ChatViewModel(
|
||||
messageManager = messageManager,
|
||||
privateChatManager = privateChatManager,
|
||||
meshDelegateHandler = meshDelegateHandler,
|
||||
coroutineScope = viewModelScope
|
||||
coroutineScope = viewModelScope,
|
||||
dataManager = dataManager
|
||||
)
|
||||
|
||||
// Expose state through LiveData (maintaining the same interface)
|
||||
@@ -133,6 +134,7 @@ class ChatViewModel(
|
||||
dataManager.loadFavorites()
|
||||
state.setFavoritePeers(dataManager.favoritePeers.toSet())
|
||||
dataManager.loadBlockedUsers()
|
||||
dataManager.loadGeohashBlockedUsers()
|
||||
|
||||
// Log all favorites at startup
|
||||
dataManager.logAllFavorites()
|
||||
@@ -583,6 +585,13 @@ class ChatViewModel(
|
||||
nostrGeohashService.selectLocationChannel(channel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Block a user in geohash channels by their nickname
|
||||
*/
|
||||
fun blockUserInGeohash(targetNickname: String) {
|
||||
nostrGeohashService.blockUserInGeohash(targetNickname)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -209,12 +209,41 @@ class DataManager(private val context: Context) {
|
||||
return _blockedUsers.contains(fingerprint)
|
||||
}
|
||||
|
||||
// MARK: - Geohash Blocked Users Management
|
||||
|
||||
private val _geohashBlockedUsers = mutableSetOf<String>() // Set of nostr pubkey hex
|
||||
val geohashBlockedUsers: Set<String> get() = _geohashBlockedUsers.toSet()
|
||||
|
||||
fun loadGeohashBlockedUsers() {
|
||||
val savedGeohashBlockedUsers = prefs.getStringSet("geohash_blocked_users", emptySet()) ?: emptySet()
|
||||
_geohashBlockedUsers.addAll(savedGeohashBlockedUsers)
|
||||
}
|
||||
|
||||
fun saveGeohashBlockedUsers() {
|
||||
prefs.edit().putStringSet("geohash_blocked_users", _geohashBlockedUsers).apply()
|
||||
}
|
||||
|
||||
fun addGeohashBlockedUser(pubkeyHex: String) {
|
||||
_geohashBlockedUsers.add(pubkeyHex)
|
||||
saveGeohashBlockedUsers()
|
||||
}
|
||||
|
||||
fun removeGeohashBlockedUser(pubkeyHex: String) {
|
||||
_geohashBlockedUsers.remove(pubkeyHex)
|
||||
saveGeohashBlockedUsers()
|
||||
}
|
||||
|
||||
fun isGeohashUserBlocked(pubkeyHex: String): Boolean {
|
||||
return _geohashBlockedUsers.contains(pubkeyHex)
|
||||
}
|
||||
|
||||
// MARK: - Emergency Clear
|
||||
|
||||
fun clearAllData() {
|
||||
_channelCreators.clear()
|
||||
_favoritePeers.clear()
|
||||
_blockedUsers.clear()
|
||||
_geohashBlockedUsers.clear()
|
||||
_channelMembers.clear()
|
||||
prefs.edit().clear().apply()
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.bitchat.android.ui
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -32,7 +35,9 @@ fun MessagesList(
|
||||
currentUserNickname: String,
|
||||
meshService: BluetoothMeshService,
|
||||
modifier: Modifier = Modifier,
|
||||
forceScrollToBottom: Boolean = false
|
||||
forceScrollToBottom: Boolean = false,
|
||||
onNicknameClick: ((String) -> Unit)? = null,
|
||||
onNicknameLongPress: ((String) -> Unit)? = null
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
@@ -76,18 +81,23 @@ fun MessagesList(
|
||||
MessageItem(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
meshService = meshService
|
||||
meshService = meshService,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onNicknameLongPress = onNicknameLongPress
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MessageItem(
|
||||
message: BitchatMessage,
|
||||
currentUserNickname: String,
|
||||
meshService: BluetoothMeshService
|
||||
meshService: BluetoothMeshService,
|
||||
onNicknameClick: ((String) -> Unit)? = null,
|
||||
onNicknameLongPress: ((String) -> Unit)? = null
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val timeFormatter = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
|
||||
@@ -97,19 +107,16 @@ fun MessageItem(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
// Single text view for natural wrapping (like iOS)
|
||||
Text(
|
||||
text = formatMessageAsAnnotatedString(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
meshService = meshService,
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter
|
||||
),
|
||||
modifier = Modifier.weight(1f),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible
|
||||
// Create a custom layout that combines selectable text with clickable nickname areas
|
||||
MessageTextWithClickableNicknames(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
meshService = meshService,
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter,
|
||||
onNicknameClick = onNicknameClick,
|
||||
onNicknameLongPress = onNicknameLongPress,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
// Delivery status for private messages
|
||||
@@ -121,6 +128,85 @@ fun MessageItem(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun MessageTextWithClickableNicknames(
|
||||
message: BitchatMessage,
|
||||
currentUserNickname: String,
|
||||
meshService: BluetoothMeshService,
|
||||
colorScheme: ColorScheme,
|
||||
timeFormatter: SimpleDateFormat,
|
||||
onNicknameClick: ((String) -> Unit)?,
|
||||
onNicknameLongPress: ((String) -> Unit)?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val annotatedText = formatMessageAsAnnotatedString(
|
||||
message = message,
|
||||
currentUserNickname = currentUserNickname,
|
||||
meshService = meshService,
|
||||
colorScheme = colorScheme,
|
||||
timeFormatter = timeFormatter
|
||||
)
|
||||
|
||||
// Check if this message was sent by self to avoid click interactions on own nickname
|
||||
val isSelf = message.senderPeerID == meshService.myPeerID ||
|
||||
message.sender == currentUserNickname ||
|
||||
message.sender.startsWith("$currentUserNickname#")
|
||||
|
||||
if (!isSelf && (onNicknameClick != null || onNicknameLongPress != null)) {
|
||||
// Use Text with combinedClickable for nickname interactions
|
||||
Text(
|
||||
text = annotatedText,
|
||||
modifier = modifier.combinedClickable(
|
||||
onClick = {
|
||||
// We can't get the click offset here, so we'll handle the first nickname
|
||||
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
||||
tag = "nickname_click",
|
||||
start = 0,
|
||||
end = annotatedText.length
|
||||
)
|
||||
if (nicknameAnnotations.isNotEmpty()) {
|
||||
val nickname = nicknameAnnotations.first().item
|
||||
onNicknameClick?.invoke(nickname)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// Handle long press for the first nickname
|
||||
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
||||
tag = "nickname_click",
|
||||
start = 0,
|
||||
end = annotatedText.length
|
||||
)
|
||||
if (nicknameAnnotations.isNotEmpty()) {
|
||||
val nickname = nicknameAnnotations.first().item
|
||||
onNicknameLongPress?.invoke(nickname)
|
||||
}
|
||||
}
|
||||
),
|
||||
fontFamily = FontFamily.Monospace,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// Use selectable text when no interactions needed
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = annotatedText,
|
||||
modifier = modifier,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
softWrap = true,
|
||||
overflow = TextOverflow.Visible,
|
||||
style = androidx.compose.ui.text.TextStyle(
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeliveryStatusIcon(status: DeliveryStatus) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
|
||||
Reference in New Issue
Block a user