slightly increase padding (#295)

* slightly increase padding

* better colors

* add copy to bottom sheet

* increase font size

* base font size

* anchor chat at the bottom
This commit is contained in:
callebtc
2025-08-23 14:14:42 +02:00
committed by GitHub
parent 26520991cf
commit a7604d9026
10 changed files with 185 additions and 144 deletions
@@ -14,6 +14,7 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import com.bitchat.android.model.BitchatMessage
/** /**
* Main ChatScreen - REFACTORED to use component-based architecture * Main ChatScreen - REFACTORED to use component-based architecture
@@ -52,6 +53,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
var showLocationChannelsSheet by remember { mutableStateOf(false) } var showLocationChannelsSheet by remember { mutableStateOf(false) }
var showUserSheet by remember { mutableStateOf(false) } var showUserSheet by remember { mutableStateOf(false) }
var selectedUserForSheet by remember { mutableStateOf("") } var selectedUserForSheet by remember { mutableStateOf("") }
var selectedMessageForSheet by remember { mutableStateOf<BitchatMessage?>(null) }
var forceScrollToBottom by remember { mutableStateOf(false) } var forceScrollToBottom by remember { mutableStateOf(false) }
// Show password dialog when needed // Show password dialog when needed
@@ -126,11 +128,12 @@ fun ChatScreen(viewModel: ChatViewModel) {
selection = TextRange(newText.length) selection = TextRange(newText.length)
) )
}, },
onNicknameLongPress = { fullSenderName -> onMessageLongPress = { message ->
// Long press - open user action sheet // Message long press - open user action sheet with message context
// Extract base nickname from full sender name // Extract base nickname from message sender (contains all necessary info)
val (baseName, _) = splitSuffix(fullSenderName) val (baseName, _) = splitSuffix(message.sender)
selectedUserForSheet = baseName selectedUserForSheet = baseName
selectedMessageForSheet = message
showUserSheet = true showUserSheet = true
} }
) )
@@ -261,8 +264,12 @@ fun ChatScreen(viewModel: ChatViewModel) {
showLocationChannelsSheet = showLocationChannelsSheet, showLocationChannelsSheet = showLocationChannelsSheet,
onLocationChannelsSheetDismiss = { showLocationChannelsSheet = false }, onLocationChannelsSheetDismiss = { showLocationChannelsSheet = false },
showUserSheet = showUserSheet, showUserSheet = showUserSheet,
onUserSheetDismiss = { showUserSheet = false }, onUserSheetDismiss = {
showUserSheet = false
selectedMessageForSheet = null // Reset message when dismissing
},
selectedUserForSheet = selectedUserForSheet, selectedUserForSheet = selectedUserForSheet,
selectedMessageForSheet = selectedMessageForSheet,
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -387,6 +394,7 @@ private fun ChatDialogs(
showUserSheet: Boolean, showUserSheet: Boolean,
onUserSheetDismiss: () -> Unit, onUserSheetDismiss: () -> Unit,
selectedUserForSheet: String, selectedUserForSheet: String,
selectedMessageForSheet: BitchatMessage?,
viewModel: ChatViewModel viewModel: ChatViewModel
) { ) {
// Password dialog // Password dialog
@@ -420,6 +428,7 @@ private fun ChatDialogs(
isPresented = showUserSheet, isPresented = showUserSheet,
onDismiss = onUserSheetDismiss, onDismiss = onUserSheetDismiss,
targetNickname = selectedUserForSheet, targetNickname = selectedUserForSheet,
selectedMessage = selectedMessageForSheet,
viewModel = viewModel viewModel = viewModel
) )
} }
@@ -9,6 +9,7 @@ import androidx.compose.ui.unit.sp
import com.bitchat.android.model.BitchatMessage import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.mesh.BluetoothMeshService import com.bitchat.android.mesh.BluetoothMeshService
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@@ -63,7 +64,7 @@ fun formatMessageAsAnnotatedString(
// Sender prefix "<@" // Sender prefix "<@"
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor, color = baseColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
)) ))
builder.append("<@") builder.append("<@")
@@ -72,7 +73,7 @@ fun formatMessageAsAnnotatedString(
// Base name (clickable) // Base name (clickable)
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor, color = baseColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
)) ))
val nicknameStart = builder.length val nicknameStart = builder.length
@@ -94,7 +95,7 @@ fun formatMessageAsAnnotatedString(
if (suffix.isNotEmpty()) { if (suffix.isNotEmpty()) {
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor.copy(alpha = 0.6f), color = baseColor.copy(alpha = 0.6f),
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
)) ))
builder.append(suffix) builder.append(suffix)
@@ -104,7 +105,7 @@ fun formatMessageAsAnnotatedString(
// Sender suffix "> " // Sender suffix "> "
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor, color = baseColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Medium
)) ))
builder.append("> ") builder.append("> ")
@@ -116,7 +117,7 @@ fun formatMessageAsAnnotatedString(
// iOS-style timestamp at the END (smaller, grey) // iOS-style timestamp at the END (smaller, grey)
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = Color.Gray.copy(alpha = 0.7f), color = Color.Gray.copy(alpha = 0.7f),
fontSize = 10.sp fontSize = (BASE_FONT_SIZE - 4).sp
)) ))
builder.append(" [${timeFormatter.format(message.timestamp)}]") builder.append(" [${timeFormatter.format(message.timestamp)}]")
builder.pop() builder.pop()
@@ -125,7 +126,7 @@ fun formatMessageAsAnnotatedString(
// System message - iOS style // System message - iOS style
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = Color.Gray, color = Color.Gray,
fontSize = 12.sp, fontSize = (BASE_FONT_SIZE - 2).sp,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic fontStyle = androidx.compose.ui.text.font.FontStyle.Italic
)) ))
builder.append("* ${message.content} *") builder.append("* ${message.content} *")
@@ -134,7 +135,7 @@ fun formatMessageAsAnnotatedString(
// Timestamp for system messages too // Timestamp for system messages too
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = Color.Gray.copy(alpha = 0.5f), color = Color.Gray.copy(alpha = 0.5f),
fontSize = 10.sp fontSize = (BASE_FONT_SIZE - 4).sp
)) ))
builder.append(" [${timeFormatter.format(message.timestamp)}]") builder.append(" [${timeFormatter.format(message.timestamp)}]")
builder.pop() builder.pop()
@@ -190,7 +191,7 @@ fun colorForPeerSeed(seed: String, isDark: Boolean): Color {
} }
val saturation = if (isDark) 0.50 else 0.70 val saturation = if (isDark) 0.50 else 0.70
val brightness = if (isDark) 0.95 else 0.45 val brightness = if (isDark) 0.85 else 0.35
return Color.hsv( return Color.hsv(
hue = (hue * 360).toFloat(), hue = (hue * 360).toFloat(),
@@ -269,7 +270,7 @@ private fun appendIOSFormattedContent(
if (beforeText.isNotEmpty()) { if (beforeText.isNotEmpty()) {
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor, color = baseColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal
)) ))
if (isMentioned) { if (isMentioned) {
@@ -299,7 +300,7 @@ private fun appendIOSFormattedContent(
// "@" symbol // "@" symbol
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = mentionColor, color = mentionColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold
)) ))
builder.append("@") builder.append("@")
@@ -308,7 +309,7 @@ private fun appendIOSFormattedContent(
// Base name // Base name
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = mentionColor, color = mentionColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold
)) ))
builder.append(mBase) builder.append(mBase)
@@ -318,7 +319,7 @@ private fun appendIOSFormattedContent(
if (mSuffix.isNotEmpty()) { if (mSuffix.isNotEmpty()) {
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = mentionColor.copy(alpha = 0.6f), color = mentionColor.copy(alpha = 0.6f),
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold fontWeight = if (isSelf) FontWeight.Bold else FontWeight.SemiBold
)) ))
builder.append(mSuffix) builder.append(mSuffix)
@@ -329,7 +330,7 @@ private fun appendIOSFormattedContent(
// iOS-style: render hashtags like normal content (no special styling) // iOS-style: render hashtags like normal content (no special styling)
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor, color = baseColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal
)) ))
if (isMentioned) { if (isMentioned) {
@@ -351,7 +352,7 @@ private fun appendIOSFormattedContent(
val remainingText = content.substring(lastEnd) val remainingText = content.substring(lastEnd)
builder.pushStyle(SpanStyle( builder.pushStyle(SpanStyle(
color = baseColor, color = baseColor,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal fontWeight = if (isSelf) FontWeight.Bold else FontWeight.Normal
)) ))
if (isMentioned) { if (isMentioned) {
@@ -11,7 +11,11 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.bitchat.android.model.BitchatMessage
/** /**
* User Action Sheet for selecting actions on a specific user (slap, hug, block) * User Action Sheet for selecting actions on a specific user (slap, hug, block)
@@ -23,10 +27,12 @@ fun ChatUserSheet(
isPresented: Boolean, isPresented: Boolean,
onDismiss: () -> Unit, onDismiss: () -> Unit,
targetNickname: String, targetNickname: String,
selectedMessage: BitchatMessage? = null,
viewModel: ChatViewModel, viewModel: ChatViewModel,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
// Bottom sheet state // Bottom sheet state
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
@@ -39,6 +45,7 @@ fun ChatUserSheet(
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green
val standardBlue = Color(0xFF007AFF) // iOS blue val standardBlue = Color(0xFF007AFF) // iOS blue
val standardRed = Color(0xFFFF3B30) // iOS red val standardRed = Color(0xFFFF3B30) // iOS red
val standardGrey = if (isDark) Color(0xFF8E8E93) else Color(0xFF6D6D70) // iOS grey
if (isPresented) { if (isPresented) {
ModalBottomSheet( ModalBottomSheet(
@@ -62,7 +69,7 @@ fun ChatUserSheet(
) )
Text( Text(
text = "choose an action for this user", text = if (selectedMessage != null) "choose an action for this message or user" else "choose an action for this user",
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)
@@ -72,53 +79,72 @@ fun ChatUserSheet(
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
// Slap action // Copy message action (only show if we have a message)
item { selectedMessage?.let { message ->
UserActionRow( item {
title = "slap $targetNickname", UserActionRow(
subtitle = "send a playful slap message", title = "copy message",
titleColor = standardBlue, subtitle = "copy this message to clipboard",
onClick = { titleColor = standardGrey,
// Send slap command onClick = {
viewModel.sendMessage("/slap $targetNickname") // Copy the message content to clipboard
onDismiss() clipboardManager.setText(AnnotatedString(message.content))
} 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() )
} }
) }
// Only show user actions for other users' messages or when no message is selected
if (selectedMessage?.sender != viewModel.nickname.value) {
// 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()
}
)
}
} }
} }
@@ -133,7 +159,7 @@ fun ChatUserSheet(
) { ) {
Text( Text(
text = "cancel", text = "cancel",
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
) )
} }
@@ -164,7 +190,7 @@ private fun UserActionRow(
) { ) {
Text( Text(
text = title, text = title,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = titleColor color = titleColor
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import java.util.* import java.util.*
/** /**
@@ -77,7 +78,7 @@ fun GeohashPeopleList(
text = "nobody around...", text = "nobody around...",
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp fontSize = BASE_FONT_SIZE.sp
), ),
color = colorScheme.onSurface.copy(alpha = 0.5f), color = colorScheme.onSurface.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp) modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp)
@@ -212,7 +213,7 @@ private fun GeohashPersonItem(
text = baseName, text = baseName,
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal
), ),
color = baseColor color = baseColor
@@ -224,7 +225,7 @@ private fun GeohashPersonItem(
text = suffix, text = suffix,
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp fontSize = BASE_FONT_SIZE.sp
), ),
color = baseColor.copy(alpha = 0.6f) color = baseColor.copy(alpha = 0.6f)
) )
@@ -236,7 +237,7 @@ private fun GeohashPersonItem(
text = " (you)", text = " (you)",
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp fontSize = BASE_FONT_SIZE.sp
), ),
color = baseColor color = baseColor
) )
@@ -34,6 +34,7 @@ import androidx.compose.ui.unit.sp
import com.bitchat.android.R import com.bitchat.android.R
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
/** /**
* Input components for ChatScreen * Input components for ChatScreen
@@ -326,7 +327,7 @@ fun CommandSuggestionItem(
fontWeight = FontWeight.Medium fontWeight = FontWeight.Medium
), ),
color = colorScheme.primary, color = colorScheme.primary,
fontSize = 11.sp fontSize = (BASE_FONT_SIZE - 4).sp
) )
// Show syntax if any // Show syntax if any
@@ -337,7 +338,7 @@ fun CommandSuggestionItem(
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
), ),
color = colorScheme.onSurface.copy(alpha = 0.8f), color = colorScheme.onSurface.copy(alpha = 0.8f),
fontSize = 10.sp fontSize = (BASE_FONT_SIZE - 5).sp
) )
} }
@@ -348,7 +349,7 @@ fun CommandSuggestionItem(
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
), ),
color = colorScheme.onSurface.copy(alpha = 0.7f), color = colorScheme.onSurface.copy(alpha = 0.7f),
fontSize = 10.sp, fontSize = (BASE_FONT_SIZE - 5).sp,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
@@ -400,7 +401,7 @@ fun MentionSuggestionItem(
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
), ),
color = Color(0xFFFF9500), // Orange like mentions color = Color(0xFFFF9500), // Orange like mentions
fontSize = 11.sp fontSize = (BASE_FONT_SIZE - 4).sp
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -411,7 +412,7 @@ fun MentionSuggestionItem(
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
), ),
color = colorScheme.onSurface.copy(alpha = 0.7f), color = colorScheme.onSurface.copy(alpha = 0.7f),
fontSize = 10.sp fontSize = (BASE_FONT_SIZE - 5).sp
) )
} }
} }
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import java.net.URL import java.net.URL
/** /**
@@ -109,7 +110,7 @@ fun LinkPreviewPill(
Text( Text(
text = displayTitle, text = displayTitle,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = textColor, color = textColor,
maxLines = 2, maxLines = 2,
@@ -23,6 +23,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.bitchat.android.geohash.ChannelID import com.bitchat.android.geohash.ChannelID
import com.bitchat.android.geohash.GeohashChannel import com.bitchat.android.geohash.GeohashChannel
@@ -240,7 +241,7 @@ fun LocationChannelsSheet(
) { ) {
Text( Text(
text = "#", text = "#",
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
) )
@@ -260,7 +261,7 @@ fun LocationChannelsSheet(
customError = null customError = null
}, },
textStyle = androidx.compose.ui.text.TextStyle( textStyle = androidx.compose.ui.text.TextStyle(
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
), ),
@@ -283,7 +284,7 @@ fun LocationChannelsSheet(
if (customGeohash.isEmpty()) { if (customGeohash.isEmpty()) {
Text( Text(
text = "geohash", text = "geohash",
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
) )
@@ -321,7 +322,7 @@ fun LocationChannelsSheet(
) { ) {
Text( Text(
text = "teleport", text = "teleport",
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace fontFamily = FontFamily.Monospace
) )
// iOS has a face.dashed icon, use closest Material equivalent // iOS has a face.dashed icon, use closest Material equivalent
@@ -446,7 +447,7 @@ private fun ChannelRow(
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
Text( Text(
text = baseTitle, text = baseTitle,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = if (titleBold) FontWeight.Bold else FontWeight.Normal, fontWeight = if (titleBold) FontWeight.Bold else FontWeight.Normal,
color = titleColor ?: MaterialTheme.colorScheme.onSurface color = titleColor ?: MaterialTheme.colorScheme.onSurface
@@ -7,7 +7,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -37,7 +37,7 @@ fun MessagesList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
forceScrollToBottom: Boolean = false, forceScrollToBottom: Boolean = false,
onNicknameClick: ((String) -> Unit)? = null, onNicknameClick: ((String) -> Unit)? = null,
onNicknameLongPress: ((String) -> Unit)? = null onMessageLongPress: ((BitchatMessage) -> Unit)? = null
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -48,15 +48,14 @@ fun MessagesList(
LaunchedEffect(messages.size) { LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) { if (messages.isNotEmpty()) {
val layoutInfo = listState.layoutInfo val layoutInfo = listState.layoutInfo
val lastVisibleIndex = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1 val firstVisibleIndex = layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: -1
val totalItems = layoutInfo.totalItemsCount
// Always scroll to bottom on first load, or when user is near the bottom // With reverseLayout=true and reversed data, index 0 is the latest message at the bottom
val isFirstLoad = !hasScrolledToInitialPosition val isFirstLoad = !hasScrolledToInitialPosition
val isNearBottom = lastVisibleIndex >= totalItems - 3 val isNearLatest = firstVisibleIndex <= 2
if (isFirstLoad || isNearBottom) { if (isFirstLoad || isNearLatest) {
listState.animateScrollToItem(messages.size - 1) listState.animateScrollToItem(0)
if (isFirstLoad) { if (isFirstLoad) {
hasScrolledToInitialPosition = true hasScrolledToInitialPosition = true
} }
@@ -67,25 +66,26 @@ fun MessagesList(
// Force scroll to bottom when requested (e.g., when user sends a message) // Force scroll to bottom when requested (e.g., when user sends a message)
LaunchedEffect(forceScrollToBottom) { LaunchedEffect(forceScrollToBottom) {
if (forceScrollToBottom && messages.isNotEmpty()) { if (forceScrollToBottom && messages.isNotEmpty()) {
listState.animateScrollToItem(messages.size - 1) // With reverseLayout=true and reversed data, latest is at index 0
listState.animateScrollToItem(0)
} }
} }
SelectionContainer(modifier = modifier) { LazyColumn(
LazyColumn( state = listState,
state = listState, contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp),
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp) modifier = modifier,
) { reverseLayout = true
items(messages) { message -> ) {
items(messages.asReversed()) { message ->
MessageItem( MessageItem(
message = message, message = message,
currentUserNickname = currentUserNickname, currentUserNickname = currentUserNickname,
meshService = meshService, meshService = meshService,
onNicknameClick = onNicknameClick, onNicknameClick = onNicknameClick,
onNicknameLongPress = onNicknameLongPress onMessageLongPress = onMessageLongPress
) )
}
} }
} }
} }
@@ -97,7 +97,7 @@ fun MessageItem(
currentUserNickname: String, currentUserNickname: String,
meshService: BluetoothMeshService, meshService: BluetoothMeshService,
onNicknameClick: ((String) -> Unit)? = null, onNicknameClick: ((String) -> Unit)? = null,
onNicknameLongPress: ((String) -> Unit)? = null onMessageLongPress: ((BitchatMessage) -> Unit)? = null
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val timeFormatter = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) } val timeFormatter = remember { SimpleDateFormat("HH:mm:ss", Locale.getDefault()) }
@@ -119,7 +119,7 @@ fun MessageItem(
colorScheme = colorScheme, colorScheme = colorScheme,
timeFormatter = timeFormatter, timeFormatter = timeFormatter,
onNicknameClick = onNicknameClick, onNicknameClick = onNicknameClick,
onNicknameLongPress = onNicknameLongPress, onMessageLongPress = onMessageLongPress,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
@@ -164,7 +164,7 @@ private fun MessageTextWithClickableNicknames(
colorScheme: ColorScheme, colorScheme: ColorScheme,
timeFormatter: SimpleDateFormat, timeFormatter: SimpleDateFormat,
onNicknameClick: ((String) -> Unit)?, onNicknameClick: ((String) -> Unit)?,
onNicknameLongPress: ((String) -> Unit)?, onMessageLongPress: ((BitchatMessage) -> Unit)?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val annotatedText = formatMessageAsAnnotatedString( val annotatedText = formatMessageAsAnnotatedString(
@@ -180,8 +180,8 @@ private fun MessageTextWithClickableNicknames(
message.sender == currentUserNickname || message.sender == currentUserNickname ||
message.sender.startsWith("$currentUserNickname#") message.sender.startsWith("$currentUserNickname#")
if (!isSelf && (onNicknameClick != null || onNicknameLongPress != null)) { if (!isSelf && (onNicknameClick != null || onMessageLongPress != null)) {
// Use Text with combinedClickable for nickname interactions // Use Text with combinedClickable for nickname interactions and message long press
Text( Text(
text = annotatedText, text = annotatedText,
modifier = modifier.combinedClickable( modifier = modifier.combinedClickable(
@@ -198,16 +198,8 @@ private fun MessageTextWithClickableNicknames(
} }
}, },
onLongClick = { onLongClick = {
// Handle long press for the first nickname // Always use message long press - contains all necessary information
val nicknameAnnotations = annotatedText.getStringAnnotations( onMessageLongPress?.invoke(message)
tag = "nickname_click",
start = 0,
end = annotatedText.length
)
if (nicknameAnnotations.isNotEmpty()) {
val nickname = nicknameAnnotations.first().item
onNicknameLongPress?.invoke(nickname)
}
} }
), ),
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
@@ -218,19 +210,24 @@ private fun MessageTextWithClickableNicknames(
) )
) )
} else { } else {
// Use selectable text when no interactions needed // Use regular text with message long press support for own messages
SelectionContainer { Text(
Text( text = annotatedText,
text = annotatedText, modifier = if (onMessageLongPress != null) {
modifier = modifier, modifier.combinedClickable(
fontFamily = FontFamily.Monospace, onClick = { /* No action for own messages */ },
softWrap = true, onLongClick = { onMessageLongPress.invoke(message) }
overflow = TextOverflow.Visible,
style = androidx.compose.ui.text.TextStyle(
color = colorScheme.onSurface
) )
} else {
modifier
},
fontFamily = FontFamily.Monospace,
softWrap = true,
overflow = TextOverflow.Visible,
style = androidx.compose.ui.text.TextStyle(
color = colorScheme.onSurface
) )
} )
} }
} }
@@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.bitchat.android.ui.theme.BASE_FONT_SIZE
/** /**
@@ -400,7 +401,7 @@ private fun PeerItem(
text = baseName, text = baseName,
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal fontWeight = if (isMe) FontWeight.Bold else FontWeight.Normal
), ),
color = baseColor color = baseColor
@@ -412,7 +413,7 @@ private fun PeerItem(
text = suffix, text = suffix,
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontSize = 14.sp fontSize = BASE_FONT_SIZE.sp
), ),
color = baseColor.copy(alpha = 0.6f) color = baseColor.copy(alpha = 0.6f)
) )
@@ -6,48 +6,51 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
// Typography matching the iOS monospace design - increased font sizes for better readability // Base font size for consistent scaling across the app
const val BASE_FONT_SIZE = 15 // sp - increased from 14sp for better readability
// Typography matching the iOS monospace design - using BASE_FONT_SIZE for consistency
val Typography = Typography( val Typography = Typography(
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 16.sp, fontSize = (BASE_FONT_SIZE + 1).sp,
lineHeight = 22.sp lineHeight = (BASE_FONT_SIZE + 7).sp
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 14.sp, fontSize = BASE_FONT_SIZE.sp,
lineHeight = 18.sp lineHeight = (BASE_FONT_SIZE + 3).sp
), ),
bodySmall = TextStyle( bodySmall = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 12.sp, fontSize = (BASE_FONT_SIZE - 3).sp,
lineHeight = 16.sp lineHeight = (BASE_FONT_SIZE + 1).sp
), ),
headlineSmall = TextStyle( headlineSmall = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 18.sp, fontSize = (BASE_FONT_SIZE + 3).sp,
lineHeight = 24.sp lineHeight = (BASE_FONT_SIZE + 9).sp
), ),
titleMedium = TextStyle( titleMedium = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 16.sp, fontSize = (BASE_FONT_SIZE + 1).sp,
lineHeight = 22.sp lineHeight = (BASE_FONT_SIZE + 7).sp
), ),
labelMedium = TextStyle( labelMedium = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
fontSize = 13.sp, fontSize = (BASE_FONT_SIZE - 2).sp,
lineHeight = 18.sp lineHeight = (BASE_FONT_SIZE + 3).sp
), ),
labelSmall = TextStyle( labelSmall = TextStyle(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Normal, fontWeight = FontWeight.Normal,
fontSize = 11.sp, fontSize = (BASE_FONT_SIZE - 4).sp,
lineHeight = 16.sp lineHeight = (BASE_FONT_SIZE + 1).sp
) )
) )