Files
bitchat-android/app/src/main/java/com/bitchat/android/ui/AboutSheet.kt
T
callebtcandGitHub b62b15a21f Nip13 pow (#357)
* add pow

* animation

* matrix style

* animation better

* improve animation

* improve animation

* works

* fix jump

* difficulty indicator

* 10 is default

* animation runs forever

* pow in message timestamp

* pow works

* default on

* adjust animation

* no printing
2025-08-31 13:23:22 +02:00

591 lines
28 KiB
Kotlin

package com.bitchat.android.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Public
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Warning
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.nostr.NostrProofOfWork
import com.bitchat.android.nostr.PoWPreferenceManager
/**
* About Sheet for bitchat app information
* Matches the design language of LocationChannelsSheet
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutSheet(
isPresented: Boolean,
onDismiss: () -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
// Get version name from package info
val versionName = remember {
try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName
} catch (e: Exception) {
"1.0.0" // fallback version
}
}
// Bottom sheet state
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = false
)
// 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(
onDismissRequest = onDismiss,
sheetState = sheetState,
modifier = modifier
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Header
item {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Bottom
) {
Text(
text = "bitchat",
fontSize = 18.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface
)
Text(
text = "v$versionName",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.5f),
style = MaterialTheme.typography.bodySmall.copy(
baselineShift = BaselineShift(0.1f)
)
)
}
Text(
text = "decentralized mesh messaging with end-to-end encryption",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.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)
) {
Text(
text = "appearance",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
selected = themePref == com.bitchat.android.ui.theme.ThemePreference.System,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.System) },
label = { Text("system", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref == com.bitchat.android.ui.theme.ThemePreference.Light,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Light) },
label = { Text("light", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = themePref == com.bitchat.android.ui.theme.ThemePreference.Dark,
onClick = { com.bitchat.android.ui.theme.ThemePreferenceManager.set(context, com.bitchat.android.ui.theme.ThemePreference.Dark) },
label = { Text("dark", fontFamily = FontFamily.Monospace) }
)
}
}
}
// Proof of Work section
item {
val context = LocalContext.current
// Initialize PoW preferences if not already done
LaunchedEffect(Unit) {
PoWPreferenceManager.init(context)
}
val powEnabled by PoWPreferenceManager.powEnabled.collectAsState()
val powDifficulty by PoWPreferenceManager.powDifficulty.collectAsState()
Column(
modifier = Modifier.fillMaxWidth(),
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
) {
FilterChip(
selected = !powEnabled,
onClick = { PoWPreferenceManager.setPowEnabled(false) },
label = { Text("pow off", fontFamily = FontFamily.Monospace) }
)
FilterChip(
selected = powEnabled,
onClick = { PoWPreferenceManager.setPowEnabled(true) },
label = {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text("pow on", fontFamily = FontFamily.Monospace)
// Show current difficulty
if (powEnabled) {
Surface(
color = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D),
shape = RoundedCornerShape(50)
) { Box(Modifier.size(8.dp)) }
}
}
}
)
}
Text(
text = "add proof of work to geohash messages for spam deterrence.",
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
// Show difficulty slider when enabled
if (powEnabled) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
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..20f,
steps = 21, // 20 discrete values (0-20)
colors = SliderDefaults.colors(
thumbColor = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D),
activeTrackColor = if (isDark) Color(0xFF32D74B) else Color(0xFF248A3D)
)
)
// Show difficulty description
Surface(
modifier = Modifier.fillMaxWidth(),
color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = "difficulty $powDifficulty requires ~${NostrProofOfWork.estimateWork(powDifficulty)} hash attempts",
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
)
Text(
text = when {
powDifficulty == 0 -> "no proof of work required"
powDifficulty <= 8 -> "very low - minimal spam protection"
powDifficulty <= 12 -> "low - basic spam protection"
powDifficulty <= 16 -> "medium - good spam protection"
powDifficulty <= 20 -> "high - strong spam protection"
powDifficulty <= 24 -> "very high - may cause delays"
else -> "extreme - significant computation required"
},
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
}
}
}
}
// Network (Tor) section
item {
val ctx = LocalContext.current
val torMode = remember { mutableStateOf(com.bitchat.android.net.TorPreferenceManager.get(ctx)) }
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)
)
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)
},
label = { Text("tor off", fontFamily = FontFamily.Monospace) }
)
FilterChip(
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)
},
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
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)) }
}
}
)
}
Text(
text = "route internet over tor for enhanced privacy.",
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
// Debug status (temporary)
Surface(
modifier = Modifier.fillMaxWidth(),
color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
shape = RoundedCornerShape(8.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Text(
text = "tor status: " +
(if (torStatus.running) "running" else "stopped") +
", bootstrap=" + torStatus.bootstrapPercent + "%",
fontSize = 11.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.75f)
)
val last = torStatus.lastLogLine
if (last.isNotEmpty()) {
Text(
text = "last: " + last.take(160),
fontSize = 10.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.6f)
)
}
}
}
}
}
// Emergency warning
item {
Surface(
modifier = Modifier.fillMaxWidth(),
color = Color.Red.copy(alpha = 0.08f),
shape = RoundedCornerShape(12.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Filled.Warning,
contentDescription = "Warning",
tint = Color(0xFFBF1A1A),
modifier = Modifier.size(16.dp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(
text = "emergency data deletion",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium,
color = Color(0xFFBF1A1A)
)
Text(
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)
)
}
}
}
}
// 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)
)
// Add extra space at bottom for gesture area
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}
}
@Composable
private fun FeatureCard(
icon: ImageVector,
iconColor: Color,
title: String,
description: String,
modifier: Modifier = Modifier
) {
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)
) {
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
)
}
}
}
}
@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)
)
}
}
/**
* Password prompt dialog for password-protected channels
* Kept as dialog since it requires user input
*/
@Composable
fun PasswordPromptDialog(
show: Boolean,
channelName: String?,
passwordInput: String,
onPasswordChange: (String) -> Unit,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
if (show && channelName != null) {
val colorScheme = MaterialTheme.colorScheme
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = "Enter Channel Password",
style = MaterialTheme.typography.titleMedium,
color = colorScheme.onSurface
)
},
text = {
Column {
Text(
text = "Channel $channelName is password protected. Enter the password to join.",
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = passwordInput,
onValueChange = onPasswordChange,
label = { Text("Password", style = MaterialTheme.typography.bodyMedium) },
textStyle = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = colorScheme.primary,
unfocusedBorderColor = colorScheme.outline
)
)
}
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(
text = "Join",
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.primary
)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(
text = "Cancel",
style = MaterialTheme.typography.bodyMedium,
color = colorScheme.onSurface
)
}
},
containerColor = colorScheme.surface,
tonalElevation = 8.dp
)
}
}