mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 22:45:22 +00:00
389 lines
14 KiB
Kotlin
389 lines
14 KiB
Kotlin
package com.bitchat.android.ui
|
|
|
|
import android.util.Log
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.text.BasicTextField
|
|
import androidx.compose.foundation.text.KeyboardActions
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.*
|
|
import androidx.compose.material.icons.outlined.*
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.runtime.livedata.observeAsState
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.graphics.Color
|
|
import androidx.compose.ui.graphics.SolidColor
|
|
import androidx.compose.ui.platform.LocalFocusManager
|
|
import androidx.compose.ui.text.font.FontFamily
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.input.ImeAction
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.ui.unit.sp
|
|
import com.bitchat.android.core.ui.utils.singleOrTripleClickable
|
|
|
|
/**
|
|
* Header components for ChatScreen
|
|
* Extracted from ChatScreen.kt for better organization
|
|
*/
|
|
|
|
@Composable
|
|
fun NicknameEditor(
|
|
value: String,
|
|
onValueChange: (String) -> Unit,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
val focusManager = LocalFocusManager.current
|
|
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = modifier
|
|
) {
|
|
Text(
|
|
text = "@",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = colorScheme.primary.copy(alpha = 0.8f)
|
|
)
|
|
|
|
BasicTextField(
|
|
value = value,
|
|
onValueChange = onValueChange,
|
|
textStyle = MaterialTheme.typography.bodyMedium.copy(
|
|
color = colorScheme.primary,
|
|
fontFamily = FontFamily.Monospace
|
|
),
|
|
cursorBrush = SolidColor(colorScheme.primary),
|
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
|
keyboardActions = KeyboardActions(
|
|
onDone = {
|
|
focusManager.clearFocus()
|
|
}
|
|
),
|
|
modifier = Modifier.widthIn(max = 100.dp)
|
|
)
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun PeerCounter(
|
|
connectedPeers: List<String>,
|
|
joinedChannels: Set<String>,
|
|
hasUnreadChannels: Map<String, Int>,
|
|
hasUnreadPrivateMessages: Set<String>,
|
|
isConnected: Boolean,
|
|
onClick: () -> Unit,
|
|
modifier: Modifier = Modifier
|
|
) {
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = modifier.clickable { onClick() }.padding(end = 8.dp) // Added right margin to match "bitchat" logo spacing
|
|
) {
|
|
if (hasUnreadChannels.values.any { it > 0 }) {
|
|
// Channel icon in a Box to ensure consistent size with other icons
|
|
Box(
|
|
modifier = Modifier.size(16.dp),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
Text(
|
|
text = "#",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = Color(0xFF0080FF),
|
|
fontSize = 16.sp
|
|
)
|
|
}
|
|
Spacer(modifier = Modifier.width(6.dp))
|
|
}
|
|
|
|
if (hasUnreadPrivateMessages.isNotEmpty()) {
|
|
// Filled mail icon to match sidebar style
|
|
Icon(
|
|
imageVector = Icons.Filled.Email,
|
|
contentDescription = "Unread private messages",
|
|
modifier = Modifier.size(16.dp),
|
|
tint = Color(0xFFFF9500) // Orange to match private message theme
|
|
)
|
|
Spacer(modifier = Modifier.width(6.dp))
|
|
}
|
|
|
|
Icon(
|
|
imageVector = Icons.Default.Person,
|
|
contentDescription = "Connected peers",
|
|
modifier = Modifier.size(16.dp),
|
|
tint = if (isConnected) Color(0xFF00C851) else Color.Red
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(
|
|
text = "${connectedPeers.size}",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = if (isConnected) Color(0xFF00C851) else Color.Red,
|
|
fontSize = 16.sp,
|
|
fontWeight = FontWeight.Medium
|
|
)
|
|
|
|
if (joinedChannels.isNotEmpty()) {
|
|
Text(
|
|
text = " · ⧉ ${joinedChannels.size}",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = if (isConnected) Color(0xFF00C851) else Color.Red,
|
|
fontSize = 16.sp,
|
|
fontWeight = FontWeight.Medium
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ChatHeaderContent(
|
|
selectedPrivatePeer: String?,
|
|
currentChannel: String?,
|
|
nickname: String,
|
|
viewModel: ChatViewModel,
|
|
onBackClick: () -> Unit,
|
|
onSidebarClick: () -> Unit,
|
|
onTripleClick: () -> Unit,
|
|
onShowAppInfo: () -> Unit
|
|
) {
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
|
|
when {
|
|
selectedPrivatePeer != null -> {
|
|
// Private chat header - ensure state synchronization
|
|
val favoritePeers by viewModel.favoritePeers.observeAsState(emptySet())
|
|
val fingerprint = viewModel.privateChatManager.getPeerFingerprint(selectedPrivatePeer)
|
|
val isFavorite = favoritePeers.contains(fingerprint)
|
|
|
|
Log.d("ChatHeader", "Header recomposing: peer=$selectedPrivatePeer, fingerprint=$fingerprint, isFav=$isFavorite")
|
|
|
|
PrivateChatHeader(
|
|
peerID = selectedPrivatePeer,
|
|
peerNicknames = viewModel.meshService.getPeerNicknames(),
|
|
isFavorite = isFavorite,
|
|
onBackClick = onBackClick,
|
|
onToggleFavorite = { viewModel.toggleFavorite(selectedPrivatePeer) }
|
|
)
|
|
}
|
|
currentChannel != null -> {
|
|
// Channel header
|
|
ChannelHeader(
|
|
channel = currentChannel,
|
|
onBackClick = onBackClick,
|
|
onLeaveChannel = { viewModel.leaveChannel(currentChannel) },
|
|
onSidebarClick = onSidebarClick
|
|
)
|
|
}
|
|
else -> {
|
|
// Main header
|
|
MainHeader(
|
|
nickname = nickname,
|
|
onNicknameChange = viewModel::setNickname,
|
|
onTitleClick = onShowAppInfo,
|
|
onTripleTitleClick = onTripleClick,
|
|
onSidebarClick = onSidebarClick,
|
|
viewModel = viewModel
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun PrivateChatHeader(
|
|
peerID: String,
|
|
peerNicknames: Map<String, String>,
|
|
isFavorite: Boolean,
|
|
onBackClick: () -> Unit,
|
|
onToggleFavorite: () -> Unit
|
|
) {
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
val peerNickname = peerNicknames[peerID] ?: peerID
|
|
|
|
Box(modifier = Modifier.fillMaxWidth()) {
|
|
// Back button - positioned all the way to the left with minimal margin
|
|
Button(
|
|
onClick = onBackClick,
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = Color.Transparent,
|
|
contentColor = colorScheme.primary
|
|
),
|
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
|
|
modifier = Modifier
|
|
.align(Alignment.CenterStart)
|
|
.offset(x = (-8).dp) // Move even further left to minimize margin
|
|
) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Filled.ArrowBack,
|
|
contentDescription = "Back",
|
|
modifier = Modifier.size(16.dp),
|
|
tint = colorScheme.primary
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(
|
|
text = "back",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = colorScheme.primary
|
|
)
|
|
}
|
|
}
|
|
|
|
// Title - perfectly centered regardless of other elements
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier.align(Alignment.Center)
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Filled.Lock,
|
|
contentDescription = "Private chat",
|
|
modifier = Modifier.size(16.dp),
|
|
tint = Color(0xFFFF9500) // Orange to match private message theme
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(
|
|
text = peerNickname,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
color = Color(0xFFFF9500) // Orange
|
|
)
|
|
}
|
|
|
|
// Favorite button - positioned on the right
|
|
IconButton(
|
|
onClick = {
|
|
Log.d("ChatHeader", "Header toggle favorite: peerID=$peerID, currentFavorite=$isFavorite")
|
|
onToggleFavorite()
|
|
},
|
|
modifier = Modifier.align(Alignment.CenterEnd)
|
|
) {
|
|
Icon(
|
|
imageVector = if (isFavorite) Icons.Filled.Star else Icons.Outlined.Star,
|
|
contentDescription = if (isFavorite) "Remove from favorites" else "Add to favorites",
|
|
modifier = Modifier.size(18.dp), // Slightly larger than sidebar icon
|
|
tint = if (isFavorite) Color(0xFFFFD700) else Color(0xFF4CAF50) // Yellow for filled, green for outlined
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ChannelHeader(
|
|
channel: String,
|
|
onBackClick: () -> Unit,
|
|
onLeaveChannel: () -> Unit,
|
|
onSidebarClick: () -> Unit
|
|
) {
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
|
|
Box(modifier = Modifier.fillMaxWidth()) {
|
|
// Back button - positioned all the way to the left with minimal margin
|
|
Button(
|
|
onClick = onBackClick,
|
|
colors = ButtonDefaults.buttonColors(
|
|
containerColor = Color.Transparent,
|
|
contentColor = colorScheme.primary
|
|
),
|
|
contentPadding = PaddingValues(horizontal = 4.dp, vertical = 4.dp), // Reduced horizontal padding
|
|
modifier = Modifier
|
|
.align(Alignment.CenterStart)
|
|
.offset(x = (-8).dp) // Move even further left to minimize margin
|
|
) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Filled.ArrowBack,
|
|
contentDescription = "Back",
|
|
modifier = Modifier.size(16.dp),
|
|
tint = colorScheme.primary
|
|
)
|
|
Spacer(modifier = Modifier.width(4.dp))
|
|
Text(
|
|
text = "back",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = colorScheme.primary
|
|
)
|
|
}
|
|
}
|
|
|
|
// Title - perfectly centered regardless of other elements
|
|
Text(
|
|
text = "channel: $channel",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
color = Color(0xFFFF9500), // Orange to match input field
|
|
modifier = Modifier
|
|
.align(Alignment.Center)
|
|
.clickable { onSidebarClick() }
|
|
)
|
|
|
|
// Leave button - positioned on the right
|
|
TextButton(
|
|
onClick = onLeaveChannel,
|
|
modifier = Modifier.align(Alignment.CenterEnd)
|
|
) {
|
|
Text(
|
|
text = "leave",
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = Color.Red
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun MainHeader(
|
|
nickname: String,
|
|
onNicknameChange: (String) -> Unit,
|
|
onTitleClick: () -> Unit,
|
|
onTripleTitleClick: () -> Unit,
|
|
onSidebarClick: () -> Unit,
|
|
viewModel: ChatViewModel
|
|
) {
|
|
val colorScheme = MaterialTheme.colorScheme
|
|
val connectedPeers by viewModel.connectedPeers.observeAsState(emptyList())
|
|
val joinedChannels by viewModel.joinedChannels.observeAsState(emptySet())
|
|
val hasUnreadChannels by viewModel.unreadChannelMessages.observeAsState(emptyMap())
|
|
val hasUnreadPrivateMessages by viewModel.unreadPrivateMessages.observeAsState(emptySet())
|
|
val isConnected by viewModel.isConnected.observeAsState(false)
|
|
|
|
Row(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
horizontalArrangement = Arrangement.SpaceBetween,
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Row(
|
|
modifier = Modifier.fillMaxHeight(),
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Text(
|
|
text = "bitchat*",
|
|
style = MaterialTheme.typography.headlineSmall,
|
|
color = colorScheme.primary,
|
|
modifier = Modifier.singleOrTripleClickable(
|
|
onSingleClick = onTitleClick,
|
|
onTripleClick = onTripleTitleClick
|
|
)
|
|
)
|
|
|
|
Spacer(modifier = Modifier.width(8.dp))
|
|
|
|
NicknameEditor(
|
|
value = nickname,
|
|
onValueChange = onNicknameChange
|
|
)
|
|
}
|
|
|
|
PeerCounter(
|
|
connectedPeers = connectedPeers.filter { it != viewModel.meshService.myPeerID },
|
|
joinedChannels = joinedChannels,
|
|
hasUnreadChannels = hasUnreadChannels,
|
|
hasUnreadPrivateMessages = hasUnreadPrivateMessages,
|
|
isConnected = isConnected,
|
|
onClick = onSidebarClick
|
|
)
|
|
}
|
|
}
|