Add App Info Bottom Sheet (#406)

* Automated update of relay data - Sun Sep  7 06:20:11 UTC 2025

* Refactor(ui): Improve AboutSheet design and layout Ios Like

This commit refactors the `AboutSheet` composable with several UI enhancements:

-   Updated the overall layout and styling for a more modern look and feel.
-   Introduced a collapsing top bar effect on scroll.
-   Replaced `FeatureCard` with individual `Row` layouts for feature descriptions.
-   Adjusted typography, spacing, and padding for better readability.
-   Updated color usage to align with `MaterialTheme.colorScheme`.
-   Made the bottom sheet skip the partially expanded state by default.
-   Added `statusBarsPadding` to the `ModalBottomSheet`.

---------

Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
ruslan_yet
2025-09-14 03:37:53 +02:00
committed by GitHub
co-authored by GitHub Action
parent 73c91b9509
commit eba0d36015
@@ -1,7 +1,11 @@
package com.bitchat.android.ui
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
@@ -16,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.BaselineShift
@@ -51,32 +56,45 @@ fun AboutSheet(
// Bottom sheet state
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false
skipPartiallyExpanded = true
)
val lazyListState = rememberLazyListState()
val isScrolled by remember {
derivedStateOf {
lazyListState.firstVisibleItemIndex > 0 || lazyListState.firstVisibleItemScrollOffset > 0
}
}
val topBarAlpha by animateFloatAsState(
targetValue = if (isScrolled) 0.95f else 0f,
label = "topBarAlpha"
)
// Color scheme matching LocationChannelsSheet
val colorScheme = MaterialTheme.colorScheme
val isDark = colorScheme.background.red + colorScheme.background.green + colorScheme.background.blue < 1.5f
val standardBlue = Color(0xFF007AFF) // iOS blue
val standardGreen = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D) // iOS green
if (isPresented) {
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = onDismiss,
sheetState = sheetState,
modifier = modifier
containerColor = MaterialTheme.colorScheme.background,
dragHandle = null
) {
Box(modifier = Modifier.fillMaxWidth()) {
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = 80.dp, bottom = 20.dp)
) {
// Header Section
item(key = "header") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Header
item {
Column(
modifier = Modifier.fillMaxWidth(),
.padding(horizontal = 24.dp)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
@@ -85,17 +103,19 @@ fun AboutSheet(
) {
Text(
text = "bitchat",
fontSize = 18.sp,
style = TextStyle(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface
fontWeight = FontWeight.Bold,
fontSize = 32.sp
),
color = MaterialTheme.colorScheme.onBackground
)
Text(
text = "v$versionName",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.5f),
color = colorScheme.onBackground.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodySmall.copy(
baselineShift = BaselineShift(0.1f)
)
@@ -106,55 +126,124 @@ fun AboutSheet(
text = "decentralized mesh messaging with end-to-end encryption",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
)
}
}
// Features section
item {
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
FeatureCard(
icon = Icons.Filled.Bluetooth,
iconColor = standardBlue,
title = "offline mesh chat",
description = "communicate directly via bluetooth le without internet or servers. messages relay through nearby devices to extend range.",
modifier = Modifier.fillMaxWidth()
)
FeatureCard(
icon = Icons.Filled.Public,
iconColor = standardGreen,
title = "online geohash channels",
description = "connect with people in your area using geohash-based channels. extend the mesh using public internet relays.",
modifier = Modifier.fillMaxWidth()
)
FeatureCard(
icon = Icons.Filled.Lock,
iconColor = if (isDark) Color(0xFFFFD60A) else Color(0xFFF5A623),
title = "end-to-end encryption",
description = "private messages are encrypted. channel messages are public.",
modifier = Modifier.fillMaxWidth()
)
}
}
// Appearance section (theme toggle)
item {
val themePref by com.bitchat.android.ui.theme.ThemePreferenceManager.themeFlow.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
item(key = "feature_offline") {
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(vertical = 8.dp)
) {
Icon(
imageVector = Icons.Filled.Bluetooth,
contentDescription = "Offline Mesh Chat",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "Offline Mesh Chat",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Communicate directly via Bluetooth LE without internet or servers. Messages relay through nearby devices to extend range.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
item(key = "feature_geohash") {
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(vertical = 8.dp)
) {
Icon(
imageVector = Icons.Default.Public,
contentDescription = "Online Geohash Channels",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "Online Geohash Channels",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Connect with people in your area using geohash-based channels. Extend the mesh using public internet relays.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
item(key = "feature_encryption") {
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(vertical = 8.dp)
) {
Icon(
imageVector = Icons.Default.Lock,
contentDescription = "End-to-End Encryption",
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = "End-to-End Encryption",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Private messages are encrypted. Channel messages are public.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
// Appearance Section
item(key = "appearance_section") {
Text(
text = "appearance",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface.copy(alpha = 0.8f)
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 24.dp, bottom = 8.dp)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val themePref by com.bitchat.android.ui.theme.ThemePreferenceManager.themeFlow.collectAsState()
Row(
modifier = Modifier.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = themePref.isSystem,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.System) },
@@ -172,13 +261,16 @@ fun AboutSheet(
)
}
}
}
// Proof of Work section
item {
val context = LocalContext.current
// Initialize PoW preferences if not already done
// Proof of Work Section
item(key = "pow_section") {
Text(
text = "proof of work",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 24.dp, bottom = 8.dp)
)
LaunchedEffect(Unit) {
PoWPreferenceManager.init(context)
}
@@ -187,17 +279,9 @@ fun AboutSheet(
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "proof of work",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
@@ -245,14 +329,13 @@ fun AboutSheet(
text = "difficulty: $powDifficulty bits (~${NostrProofOfWork.estimateMiningTime(powDifficulty)})",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
)
Slider(
value = powDifficulty.toFloat(),
onValueChange = { PoWPreferenceManager.setPowDifficulty(it.toInt()) },
valueRange = 0f..32f,
steps = 33, // 33 discrete values (0-32)
steps = 33,
colors = SliderDefaults.colors(
thumbColor = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D),
activeTrackColor = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
@@ -297,27 +380,27 @@ fun AboutSheet(
}
// Network (Tor) section
item {
val ctx = LocalContext.current
val torMode = remember { mutableStateOf(com.bitchat.android.net.TorPreferenceManager.get(ctx)) }
item(key = "network_section") {
val torMode = remember { mutableStateOf(com.bitchat.android.net.TorPreferenceManager.get(context)) }
val torStatus by com.bitchat.android.net.TorManager.statusFlow.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "network",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface.copy(alpha = 0.8f)
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier
.padding(horizontal = 24.dp)
.padding(top = 24.dp, bottom = 8.dp)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
Column(modifier = Modifier.padding(horizontal = 24.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
FilterChip(
selected = torMode.value == com.bitchat.android.net.TorMode.OFF,
onClick = {
torMode.value = com.bitchat.android.net.TorMode.OFF
com.bitchat.android.net.TorPreferenceManager.set(ctx, torMode.value)
com.bitchat.android.net.TorPreferenceManager.set(context, torMode.value)
},
label = { Text("tor off", fontFamily = FontFamily.Monospace) }
)
@@ -325,35 +408,33 @@ fun AboutSheet(
selected = torMode.value == com.bitchat.android.net.TorMode.ON,
onClick = {
torMode.value = com.bitchat.android.net.TorMode.ON
com.bitchat.android.net.TorPreferenceManager.set(ctx, torMode.value)
com.bitchat.android.net.TorPreferenceManager.set(context, torMode.value)
},
label = {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("tor on", fontFamily = FontFamily.Monospace)
// Status indicator (red/orange/green) moved inside the "tor on" button
Text("Tor On", fontFamily = FontFamily.Monospace)
val statusColor = when {
torStatus.running && torStatus.bootstrapPercent < 100 -> Color(0xFFFF9500)
torStatus.running && torStatus.bootstrapPercent >= 100 -> if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
else -> Color.Red
}
Surface(
color = statusColor,
shape = RoundedCornerShape(50)
) { Box(Modifier.size(8.dp)) }
Surface(color = statusColor, shape = CircleShape) {
Box(Modifier.size(8.dp))
}
}
}
)
}
Text(
text = "route internet over tor for enhanced privacy.",
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
if (torMode.value == com.bitchat.android.net.TorMode.ON) {
val statusText = if (torStatus.running) "Running" else "Stopped"
// Debug status (temporary)
Surface(
modifier = Modifier.fillMaxWidth(),
@@ -365,19 +446,15 @@ fun AboutSheet(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = "tor status: " +
(if (torStatus.running) "running" else "stopped") +
", bootstrap=" + torStatus.bootstrapPercent + "%",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
text = "tor Status: $statusText, bootstrap ${torStatus.bootstrapPercent}%",
style = MaterialTheme.typography.bodySmall,
color = colorScheme.onSurface.copy(alpha = 0.75f)
)
val last = torStatus.lastLogLine
if (last.isNotEmpty()) {
val lastLog = torStatus.lastLogLine
if (lastLog.isNotEmpty()) {
Text(
text = "last: " + last.take(160),
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
text = "Last: ${lastLog.take(160)}",
style = MaterialTheme.typography.labelSmall,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
@@ -385,12 +462,18 @@ fun AboutSheet(
}
}
}
}
// Emergency Warning Section
item(key = "warning_section") {
val colorScheme = MaterialTheme.colorScheme
val errorColor = colorScheme.error
// Emergency warning
item {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.Red.copy(alpha = 0.08f),
modifier = Modifier
.padding(horizontal = 24.dp, vertical = 24.dp)
.fillMaxWidth(),
color = errorColor.copy(alpha = 0.1f),
shape = RoundedCornerShape(12.dp)
) {
Row(
@@ -401,66 +484,56 @@ fun AboutSheet(
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = "Warning",
tint = Color(0xFFBF1A1A),
tint = errorColor,
modifier = Modifier.size(16.dp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = "emergency data deletion",
text = "Emergency Data Deletion",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = Color(0xFFBF1A1A)
fontWeight = FontWeight.Bold,
color = errorColor
)
Text(
text = "tip: triple-click the app title to emergency delete all stored data including messages, keys, and settings.",
text = "Tip: Triple-click the app title to emergency delete all stored data including messages, keys, and settings.",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
}
}
}
}
// Debug settings button
item {
// Footer Section
item(key = "footer") {
Column(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.padding(horizontal = 24.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Debug button styled to match the app aesthetic
if (onShowDebug != null) {
TextButton(
onClick = { onShowDebug?.invoke() },
onClick = onShowDebug,
colors = ButtonDefaults.textButtonColors(
contentColor = colorScheme.onSurface.copy(alpha = 0.6f)
contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
) {
Text(
text = "debug settings",
text = "Debug Settings",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace
)
}
}
Text(
text = "Open Source • Privacy First • Decentralized",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
}
// Version and footer space
item {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "open source • privacy first • decentralized",
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.5f)
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
)
// Add extra space at bottom for gesture area
@@ -468,79 +541,30 @@ fun AboutSheet(
}
}
}
}
}
}
@Composable
private fun FeatureCard(
icon: ImageVector,
iconColor: Color,
title: String,
description: String,
modifier: Modifier = Modifier
// TopBar
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.height(64.dp)
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = icon,
contentDescription = title,
tint = iconColor,
modifier = Modifier.size(20.dp)
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
TextButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(horizontal = 16.dp)
) {
Text(
text = title,
fontSize = 13.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = description,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
lineHeight = 15.sp
text = "Close",
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onBackground
)
}
}
}
}
@Composable
private fun FeatureItem(text: String) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Top
) {
Text(
text = "",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
Text(
text = text,
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
modifier = Modifier.weight(1f)
)
}
}