UI changes and fixes (#301)

* country -> region, region –> province

* revise clicks

* fix longpress for mention etc

* scroll down button wip

* fix distance

* fix button

* fix header icon
This commit is contained in:
callebtc
2025-08-23 21:18:36 +02:00
committed by GitHub
parent 7106be4b07
commit 37c8c79310
3 changed files with 86 additions and 46 deletions
@@ -487,30 +487,6 @@ private fun MainHeader(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
// Unread indicator (like iOS)
if (hasUnreadPrivateMessages.isNotEmpty()) {
Button(
onClick = {
// Open most relevant private chat (first unread)
val firstUnread = hasUnreadPrivateMessages.firstOrNull()
if (firstUnread != null) {
viewModel.startPrivateChat(firstUnread)
}
},
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
contentColor = Color(0xFFFF9500)
),
contentPadding = PaddingValues(4.dp)
) {
Icon(
imageVector = Icons.Filled.Email,
contentDescription = "Open unread private chat",
modifier = Modifier.size(12.dp),
tint = Color(0xFFFF9500)
)
}
}
// Location channels button (matching iOS implementation)
LocationChannelsButton(
@@ -9,6 +9,12 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.Alignment
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.IconButton
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
@@ -55,6 +61,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
var selectedUserForSheet by remember { mutableStateOf("") }
var selectedMessageForSheet by remember { mutableStateOf<BitchatMessage?>(null) }
var forceScrollToBottom by remember { mutableStateOf(false) }
var isScrolledUp by remember { mutableStateOf(false) }
// Show password dialog when needed
LaunchedEffect(showPasswordPrompt) {
@@ -100,6 +107,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
meshService = viewModel.meshService,
modifier = Modifier.weight(1f),
forceScrollToBottom = forceScrollToBottom,
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
onNicknameClick = { fullSenderName ->
// Single click - mention user in text input
val currentText = messageText.text
@@ -220,6 +228,35 @@ fun ChatScreen(viewModel: ChatViewModel) {
)
}
// Scroll-to-bottom floating button
AnimatedVisibility(
visible = isScrolledUp && !showSidebar,
enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(),
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 64.dp)
.zIndex(1.5f)
.windowInsetsPadding(WindowInsets.navigationBars)
.windowInsetsPadding(WindowInsets.ime)
) {
Surface(
shape = CircleShape,
color = colorScheme.background,
tonalElevation = 3.dp,
shadowElevation = 6.dp,
border = BorderStroke(2.dp, Color(0xFF00C851))
) {
IconButton(onClick = { forceScrollToBottom = !forceScrollToBottom }) {
Icon(
imageVector = Icons.Filled.ArrowDownward,
contentDescription = "Scroll to bottom",
tint = Color(0xFF00C851)
)
}
}
}
AnimatedVisibility(
visible = showSidebar,
enter = slideInHorizontally(
@@ -2,22 +2,27 @@ package com.bitchat.android.ui
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
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.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.input.pointer.pointerInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import com.bitchat.android.model.BitchatMessage
import com.bitchat.android.model.DeliveryStatus
import com.bitchat.android.mesh.BluetoothMeshService
@@ -36,6 +41,7 @@ fun MessagesList(
meshService: BluetoothMeshService,
modifier: Modifier = Modifier,
forceScrollToBottom: Boolean = false,
onScrolledUpChanged: ((Boolean) -> Unit)? = null,
onNicknameClick: ((String) -> Unit)? = null,
onMessageLongPress: ((BitchatMessage) -> Unit)? = null
) {
@@ -63,9 +69,20 @@ fun MessagesList(
}
}
// Track whether user has scrolled away from the latest messages
val isAtLatest by remember {
derivedStateOf {
val firstVisibleIndex = listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index ?: -1
firstVisibleIndex <= 2
}
}
LaunchedEffect(isAtLatest) {
onScrolledUpChanged?.invoke(!isAtLatest)
}
// Force scroll to bottom when requested (e.g., when user sends a message)
LaunchedEffect(forceScrollToBottom) {
if (forceScrollToBottom && messages.isNotEmpty()) {
if (messages.isNotEmpty()) {
// With reverseLayout=true and reversed data, latest is at index 0
listState.animateScrollToItem(0)
}
@@ -181,42 +198,52 @@ private fun MessageTextWithClickableNicknames(
message.sender.startsWith("$currentUserNickname#")
if (!isSelf && (onNicknameClick != null || onMessageLongPress != null)) {
// Use Text with combinedClickable for nickname interactions and message long press
val haptic = LocalHapticFeedback.current
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
Text(
text = annotatedText,
modifier = modifier.combinedClickable(
onClick = {
// We can't get the click offset here, so we'll handle the first nickname
modifier = modifier.pointerInput(message) {
detectTapGestures(
onTap = { position ->
val layout = textLayoutResult ?: return@detectTapGestures
val offset = layout.getOffsetForPosition(position)
val nicknameAnnotations = annotatedText.getStringAnnotations(
tag = "nickname_click",
start = 0,
end = annotatedText.length
start = offset,
end = offset
)
if (nicknameAnnotations.isNotEmpty()) {
val nickname = nicknameAnnotations.first().item
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
onNicknameClick?.invoke(nickname)
}
},
onLongClick = {
// Always use message long press - contains all necessary information
onLongPress = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onMessageLongPress?.invoke(message)
}
),
)
},
fontFamily = FontFamily.Monospace,
softWrap = true,
overflow = TextOverflow.Visible,
style = androidx.compose.ui.text.TextStyle(
color = colorScheme.onSurface
)
),
onTextLayout = { result -> textLayoutResult = result }
)
} else {
// Use regular text with message long press support for own messages
val haptic = LocalHapticFeedback.current
Text(
text = annotatedText,
modifier = if (onMessageLongPress != null) {
modifier.combinedClickable(
onClick = { /* No action for own messages */ },
onLongClick = { onMessageLongPress.invoke(message) }
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
onMessageLongPress.invoke(message)
}
)
} else {
modifier