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,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
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)
|
// Location channels button (matching iOS implementation)
|
||||||
LocationChannelsButton(
|
LocationChannelsButton(
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.TextRange
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -55,6 +61,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
var selectedUserForSheet by remember { mutableStateOf("") }
|
var selectedUserForSheet by remember { mutableStateOf("") }
|
||||||
var selectedMessageForSheet by remember { mutableStateOf<BitchatMessage?>(null) }
|
var selectedMessageForSheet by remember { mutableStateOf<BitchatMessage?>(null) }
|
||||||
var forceScrollToBottom by remember { mutableStateOf(false) }
|
var forceScrollToBottom by remember { mutableStateOf(false) }
|
||||||
|
var isScrolledUp by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// Show password dialog when needed
|
// Show password dialog when needed
|
||||||
LaunchedEffect(showPasswordPrompt) {
|
LaunchedEffect(showPasswordPrompt) {
|
||||||
@@ -100,6 +107,7 @@ fun ChatScreen(viewModel: ChatViewModel) {
|
|||||||
meshService = viewModel.meshService,
|
meshService = viewModel.meshService,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
forceScrollToBottom = forceScrollToBottom,
|
forceScrollToBottom = forceScrollToBottom,
|
||||||
|
onScrolledUpChanged = { isUp -> isScrolledUp = isUp },
|
||||||
onNicknameClick = { fullSenderName ->
|
onNicknameClick = { fullSenderName ->
|
||||||
// Single click - mention user in text input
|
// Single click - mention user in text input
|
||||||
val currentText = messageText.text
|
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(
|
AnimatedVisibility(
|
||||||
visible = showSidebar,
|
visible = showSidebar,
|
||||||
enter = slideInHorizontally(
|
enter = slideInHorizontally(
|
||||||
|
|||||||
@@ -2,22 +2,27 @@ package com.bitchat.android.ui
|
|||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
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.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
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.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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 androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import com.bitchat.android.model.BitchatMessage
|
import com.bitchat.android.model.BitchatMessage
|
||||||
import com.bitchat.android.model.DeliveryStatus
|
import com.bitchat.android.model.DeliveryStatus
|
||||||
import com.bitchat.android.mesh.BluetoothMeshService
|
import com.bitchat.android.mesh.BluetoothMeshService
|
||||||
@@ -36,6 +41,7 @@ fun MessagesList(
|
|||||||
meshService: BluetoothMeshService,
|
meshService: BluetoothMeshService,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceScrollToBottom: Boolean = false,
|
forceScrollToBottom: Boolean = false,
|
||||||
|
onScrolledUpChanged: ((Boolean) -> Unit)? = null,
|
||||||
onNicknameClick: ((String) -> Unit)? = null,
|
onNicknameClick: ((String) -> Unit)? = null,
|
||||||
onMessageLongPress: ((BitchatMessage) -> 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)
|
// Force scroll to bottom when requested (e.g., when user sends a message)
|
||||||
LaunchedEffect(forceScrollToBottom) {
|
LaunchedEffect(forceScrollToBottom) {
|
||||||
if (forceScrollToBottom && messages.isNotEmpty()) {
|
if (messages.isNotEmpty()) {
|
||||||
// With reverseLayout=true and reversed data, latest is at index 0
|
// With reverseLayout=true and reversed data, latest is at index 0
|
||||||
listState.animateScrollToItem(0)
|
listState.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
@@ -181,42 +198,52 @@ private fun MessageTextWithClickableNicknames(
|
|||||||
message.sender.startsWith("$currentUserNickname#")
|
message.sender.startsWith("$currentUserNickname#")
|
||||||
|
|
||||||
if (!isSelf && (onNicknameClick != null || onMessageLongPress != null)) {
|
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(
|
||||||
text = annotatedText,
|
text = annotatedText,
|
||||||
modifier = modifier.combinedClickable(
|
modifier = modifier.pointerInput(message) {
|
||||||
onClick = {
|
detectTapGestures(
|
||||||
// We can't get the click offset here, so we'll handle the first nickname
|
onTap = { position ->
|
||||||
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
val layout = textLayoutResult ?: return@detectTapGestures
|
||||||
tag = "nickname_click",
|
val offset = layout.getOffsetForPosition(position)
|
||||||
start = 0,
|
val nicknameAnnotations = annotatedText.getStringAnnotations(
|
||||||
end = annotatedText.length
|
tag = "nickname_click",
|
||||||
)
|
start = offset,
|
||||||
if (nicknameAnnotations.isNotEmpty()) {
|
end = offset
|
||||||
val nickname = nicknameAnnotations.first().item
|
)
|
||||||
onNicknameClick?.invoke(nickname)
|
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,
|
fontFamily = FontFamily.Monospace,
|
||||||
softWrap = true,
|
softWrap = true,
|
||||||
overflow = TextOverflow.Visible,
|
overflow = TextOverflow.Visible,
|
||||||
style = androidx.compose.ui.text.TextStyle(
|
style = androidx.compose.ui.text.TextStyle(
|
||||||
color = colorScheme.onSurface
|
color = colorScheme.onSurface
|
||||||
)
|
),
|
||||||
|
onTextLayout = { result -> textLayoutResult = result }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Use regular text with message long press support for own messages
|
// Use regular text with message long press support for own messages
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
Text(
|
Text(
|
||||||
text = annotatedText,
|
text = annotatedText,
|
||||||
modifier = if (onMessageLongPress != null) {
|
modifier = if (onMessageLongPress != null) {
|
||||||
modifier.combinedClickable(
|
modifier.combinedClickable(
|
||||||
onClick = { /* No action for own messages */ },
|
onClick = { /* No action for own messages */ },
|
||||||
onLongClick = { onMessageLongPress.invoke(message) }
|
onLongClick = {
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
onMessageLongPress.invoke(message)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
modifier
|
modifier
|
||||||
|
|||||||
Reference in New Issue
Block a user