mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 23:45:19 +00:00
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:
@@ -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
|
||||
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
||||
tag = "nickname_click",
|
||||
start = 0,
|
||||
end = annotatedText.length
|
||||
)
|
||||
if (nicknameAnnotations.isNotEmpty()) {
|
||||
val nickname = nicknameAnnotations.first().item
|
||||
onNicknameClick?.invoke(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 = offset,
|
||||
end = offset
|
||||
)
|
||||
if (nicknameAnnotations.isNotEmpty()) {
|
||||
val nickname = nicknameAnnotations.first().item
|
||||
haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove)
|
||||
onNicknameClick?.invoke(nickname)
|
||||
}
|
||||
},
|
||||
onLongPress = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onMessageLongPress?.invoke(message)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
// Always use message long press - contains all necessary information
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user