Redesign: Permissions screen, battery optimization, location sheet (#415)

* update permissions screen design

* battery optimization screen

* remove init screen

* skip init screen on load

* skip on load only

* location sheet design wip

* denser location sheet

* location sheet layout fix
This commit is contained in:
callebtc
2025-09-14 04:22:56 +02:00
committed by GitHub
parent 861eaaeaef
commit 1a398b16ef
5 changed files with 659 additions and 482 deletions
@@ -25,6 +25,7 @@ import com.bitchat.android.onboarding.BluetoothCheckScreen
import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatus
import com.bitchat.android.onboarding.BluetoothStatusManager import com.bitchat.android.onboarding.BluetoothStatusManager
import com.bitchat.android.onboarding.BatteryOptimizationManager import com.bitchat.android.onboarding.BatteryOptimizationManager
import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager
import com.bitchat.android.onboarding.BatteryOptimizationScreen import com.bitchat.android.onboarding.BatteryOptimizationScreen
import com.bitchat.android.onboarding.BatteryOptimizationStatus import com.bitchat.android.onboarding.BatteryOptimizationStatus
import com.bitchat.android.onboarding.InitializationErrorScreen import com.bitchat.android.onboarding.InitializationErrorScreen
@@ -163,7 +164,7 @@ class MainActivity : ComponentActivity() {
} }
when (onboardingState) { when (onboardingState) {
OnboardingState.CHECKING -> { OnboardingState.PERMISSION_REQUESTING -> {
InitializingScreen(modifier) InitializingScreen(modifier)
} }
@@ -227,15 +228,7 @@ class MainActivity : ComponentActivity() {
) )
} }
OnboardingState.PERMISSION_REQUESTING -> { OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> {
InitializingScreen(modifier)
}
OnboardingState.INITIALIZING -> {
InitializingScreen(modifier)
}
OnboardingState.COMPLETE -> {
// Set up back navigation handling for the chat screen // Set up back navigation handling for the chat screen
val backCallback = object : OnBackPressedCallback(true) { val backCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@@ -533,6 +526,13 @@ class MainActivity : ComponentActivity() {
return return
} }
// Check if user has previously skipped battery optimization
if (BatteryOptimizationPreferenceManager.isSkipped(this)) {
android.util.Log.d("MainActivity", "User previously skipped battery optimization, proceeding to permissions")
proceedWithPermissionCheck()
return
}
// For existing users, check battery optimization status // For existing users, check battery optimization status
batteryOptimizationManager.logBatteryOptimizationStatus() batteryOptimizationManager.logBatteryOptimizationStatus()
val currentBatteryOptimizationStatus = when { val currentBatteryOptimizationStatus = when {
@@ -0,0 +1,33 @@
package com.bitchat.android.onboarding
import android.content.Context
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
/**
* Preference manager for battery optimization skip choice
*/
object BatteryOptimizationPreferenceManager {
private const val PREFS_NAME = "bitchat_settings"
private const val KEY_BATTERY_SKIP = "battery_optimization_skipped"
private val _skipFlow = MutableStateFlow(false)
val skipFlow: StateFlow<Boolean> = _skipFlow
fun init(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val skipped = prefs.getBoolean(KEY_BATTERY_SKIP, false)
_skipFlow.value = skipped
}
fun setSkipped(context: Context, skipped: Boolean) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().putBoolean(KEY_BATTERY_SKIP, skipped).apply()
_skipFlow.value = skipped
}
fun isSkipped(context: Context): Boolean {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(KEY_BATTERY_SKIP, false)
}
}
@@ -3,6 +3,7 @@ package com.bitchat.android.onboarding
import androidx.compose.animation.core.* import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -12,11 +13,13 @@ 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.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.bitchat.android.R import com.bitchat.android.R
/** /**
@@ -32,10 +35,16 @@ fun BatteryOptimizationScreen(
onSkip: () -> Unit, onSkip: () -> Unit,
isLoading: Boolean = false isLoading: Boolean = false
) { ) {
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
// Initialize preference manager
LaunchedEffect(Unit) {
BatteryOptimizationPreferenceManager.init(context)
}
Box( Box(
modifier = modifier.padding(32.dp), modifier = modifier.padding(24.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
when (status) { when (status) {
@@ -73,6 +82,8 @@ private fun BatteryOptimizationEnabledContent(
colorScheme: ColorScheme, colorScheme: ColorScheme,
isLoading: Boolean isLoading: Boolean
) { ) {
val context = LocalContext.current
Column( Column(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
) { ) {
@@ -82,78 +93,112 @@ private fun BatteryOptimizationEnabledContent(
.weight(1f) .weight(1f)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(bottom = 16.dp), .padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)
horizontalAlignment = Alignment.CenterHorizontally ) {
// Header Section - matching AboutSheet style
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Text( Text(
text = "bitchat", text = "bitchat",
style = MaterialTheme.typography.headlineLarge.copy( style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = colorScheme.primary fontSize = 32.sp
) ),
) color = colorScheme.onBackground
Spacer(modifier = Modifier.height(16.dp))
Icon(
imageVector = Icons.Outlined.BatteryAlert,
contentDescription = "Battery Optimization",
modifier = Modifier.size(64.dp),
tint = colorScheme.error
) )
Text( Text(
text = stringResource(R.string.battery_optimization_detected), text = "battery optimization detected",
style = MaterialTheme.typography.headlineSmall.copy( fontSize = 12.sp,
fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface color = colorScheme.onBackground.copy(alpha = 0.7f)
),
textAlign = TextAlign.Center
) )
}
Text( // Battery optimization info section
text = stringResource(R.string.battery_optimization_explanation), Surface(
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
),
textAlign = TextAlign.Center
)
Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f) shape = RoundedCornerShape(12.dp)
)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Filled.Power,
contentDescription = "Battery Optimization",
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
)
Column {
Text( Text(
text = stringResource(R.string.battery_optimization_why_disable), text = "Battery Optimization Enabled",
style = MaterialTheme.typography.titleSmall.copy( style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.Medium,
color = colorScheme.onSurface color = colorScheme.onBackground
) )
) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.battery_optimization_benefits), text = "bitchat needs to run in the background to maintain mesh connections. battery optimization can interrupt these connections.",
style = MaterialTheme.typography.bodyMedium.copy( style = MaterialTheme.typography.bodySmall,
color = colorScheme.onSurfaceVariant color = colorScheme.onBackground.copy(alpha = 0.8f)
)
) )
} }
} }
}
}
Text( // Benefits section
text = stringResource(R.string.battery_optimization_note), Surface(
style = MaterialTheme.typography.bodySmall.copy( modifier = Modifier.fillMaxWidth(),
color = colorScheme.onSurfaceVariant color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
), shape = RoundedCornerShape(12.dp)
textAlign = TextAlign.Center ) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = Icons.Filled.CheckCircle,
contentDescription = "Benefits",
tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
) )
Column {
Text(
text = "Benefits of Disabling",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = colorScheme.onBackground
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "• reliable message delivery\n• maintains mesh connectivity\n• prevents connection drops",
style = MaterialTheme.typography.bodySmall,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
}
} }
// Fixed buttons at the bottom // Fixed buttons at the bottom
@@ -164,7 +209,10 @@ private fun BatteryOptimizationEnabledContent(
Button( Button(
onClick = onDisableBatteryOptimization, onClick = onDisableBatteryOptimization,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = !isLoading enabled = !isLoading,
colors = ButtonDefaults.buttonColors(
containerColor = colorScheme.primary
)
) { ) {
if (isLoading) { if (isLoading) {
CircularProgressIndicator( CircularProgressIndicator(
@@ -174,7 +222,13 @@ private fun BatteryOptimizationEnabledContent(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
Text(stringResource(R.string.battery_optimization_disable_button)) Text(
text = "Disable Battery Optimization",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
)
} }
Row( Row(
@@ -186,15 +240,28 @@ private fun BatteryOptimizationEnabledContent(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = !isLoading enabled = !isLoading
) { ) {
Text(stringResource(R.string.battery_optimization_check_again)) Text(
text = "Check Again",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
)
)
} }
TextButton( TextButton(
onClick = onSkip, onClick = {
BatteryOptimizationPreferenceManager.setSkipped(context, true)
onSkip()
},
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
enabled = !isLoading enabled = !isLoading
) { ) {
Text(stringResource(R.string.battery_optimization_skip)) Text(
text = "Skip for Now",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace
)
)
} }
} }
} }
@@ -206,7 +273,12 @@ private fun BatteryOptimizationCheckingContent(
colorScheme: ColorScheme colorScheme: ColorScheme
) { ) {
Column( Column(
verticalArrangement = Arrangement.spacedBy(32.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Header Section - matching AboutSheet style
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
@@ -214,9 +286,18 @@ private fun BatteryOptimizationCheckingContent(
style = MaterialTheme.typography.headlineLarge.copy( style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = colorScheme.primary fontSize = 32.sp
),
color = colorScheme.onBackground
) )
Text(
text = "battery optimization disabled",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.7f)
) )
}
val infiniteTransition = rememberInfiniteTransition(label = "rotation") val infiniteTransition = rememberInfiniteTransition(label = "rotation")
val rotation by infiniteTransition.animateFloat( val rotation by infiniteTransition.animateFloat(
@@ -239,18 +320,10 @@ private fun BatteryOptimizationCheckingContent(
) )
Text( Text(
text = stringResource(R.string.battery_optimization_disabled), text = "bitchat can run reliably in the background",
style = MaterialTheme.typography.headlineSmall.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface color = colorScheme.onBackground.copy(alpha = 0.8f)
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_success_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
), ),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@@ -265,15 +338,29 @@ private fun BatteryOptimizationNotSupportedContent(
Column( Column(
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) {
// Header Section - matching AboutSheet style
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
text = "bitchat", text = "bitchat",
style = MaterialTheme.typography.headlineLarge.copy( style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = colorScheme.primary fontSize = 32.sp
),
color = colorScheme.onBackground
) )
Text(
text = "battery optimization not required",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.7f)
) )
}
Icon( Icon(
imageVector = Icons.Filled.CheckCircle, imageVector = Icons.Filled.CheckCircle,
@@ -283,27 +370,28 @@ private fun BatteryOptimizationNotSupportedContent(
) )
Text( Text(
text = stringResource(R.string.battery_optimization_not_required), text = "your device doesn't require battery optimization settings. bitchat will run normally.",
style = MaterialTheme.typography.headlineSmall.copy( style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.SemiBold, fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface color = colorScheme.onBackground.copy(alpha = 0.8f)
),
textAlign = TextAlign.Center
)
Text(
text = stringResource(R.string.battery_optimization_not_supported_message),
style = MaterialTheme.typography.bodyLarge.copy(
color = colorScheme.onSurfaceVariant
), ),
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
Button( Button(
onClick = onRetry, onClick = onRetry,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = colorScheme.primary
)
) { ) {
Text(stringResource(R.string.battery_optimization_continue)) Text(
text = "Continue",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold
)
)
} }
} }
} }
@@ -2,12 +2,22 @@ package com.bitchat.android.onboarding
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Power
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Warning
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.graphics.vector.ImageVector
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.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -40,86 +50,87 @@ fun PermissionExplanationScreen(
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
// Header
// Header Section - matching AboutSheet style
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier
horizontalAlignment = Alignment.CenterHorizontally .fillMaxWidth()
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Bottom,
modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = "Welcome to bitchat", text = "bitchat",
style = MaterialTheme.typography.headlineMedium.copy( style = MaterialTheme.typography.headlineLarge.copy(
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = colorScheme.primary fontSize = 32.sp
), ),
textAlign = TextAlign.Center color = colorScheme.onBackground
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Decentralized mesh messaging over Bluetooth",
style = MaterialTheme.typography.bodyMedium.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.7f)
),
textAlign = TextAlign.Center
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Text(
text = "decentralized mesh messaging with end-to-end encryption",
fontSize = 12.sp,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.7f)
)
}
// Privacy assurance section // Privacy assurance section - matching AboutSheet card style
Card( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( color = colorScheme.surfaceVariant.copy(alpha = 0.25f),
containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) shape = RoundedCornerShape(12.dp)
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Text( Icon(
text = "🔒", imageVector = Icons.Filled.Security,
style = MaterialTheme.typography.titleMedium, contentDescription = "Privacy Protected",
modifier = Modifier.size(20.dp) tint = colorScheme.primary,
modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
) )
Column {
Text( Text(
text = "Your Privacy is Protected", text = "Your Privacy is Protected",
style = MaterialTheme.typography.titleSmall.copy( style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = colorScheme.onSurface
)
)
}
Text(
text = "• bitchat doesn't track you or collect personal data\n" +
"• Bluetooth mesh chats are fully offline and require no internet\n" +
"• Geohash chats use the internet but your location is generalized\n" +
"• Your messages stay on your device and peer devices only",
style = MaterialTheme.typography.bodySmall.copy(
fontFamily = FontFamily.Monospace,
color = colorScheme.onSurface.copy(alpha = 0.8f)
)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "To work properly, bitchat needs these permissions:",
style = MaterialTheme.typography.bodyMedium.copy(
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = colorScheme.onSurface color = colorScheme.onBackground
) )
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "• no tracking or data collection\n" +
"• Bluetooth mesh chats are fully offline\n" +
"• Geohash chats use the internet",
style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace,
color = colorScheme.onBackground.copy(alpha = 0.8f)
)
}
}
}
}
// Section header
Text(
text = "permissions",
style = MaterialTheme.typography.labelLarge,
color = colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
) )
// Permission categories // Permission categories
@@ -168,55 +179,46 @@ private fun PermissionCategoryCard(
category: PermissionCategory, category: PermissionCategory,
colorScheme: ColorScheme colorScheme: ColorScheme
) { ) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp) modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) { ) {
Text( Icon(
text = getPermissionEmoji(category.type), imageVector = getPermissionIcon(category.type),
style = MaterialTheme.typography.titleLarge, contentDescription = category.type.nameValue,
color = getPermissionIconColor(category.type), tint = colorScheme.primary,
modifier = Modifier.size(24.dp) modifier = Modifier
.padding(top = 2.dp)
.size(20.dp)
) )
Spacer(modifier = Modifier.width(16.dp))
Column {
Text( Text(
text = category.type.nameValue, text = category.type.nameValue,
style = MaterialTheme.typography.titleSmall.copy( style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Medium,
color = colorScheme.onSurface color = colorScheme.onBackground
) )
) Spacer(modifier = Modifier.height(4.dp))
}
Text( Text(
text = category.description, text = category.description,
style = MaterialTheme.typography.bodySmall.copy( style = MaterialTheme.typography.bodySmall,
fontFamily = FontFamily.Monospace, color = colorScheme.onBackground.copy(alpha = 0.8f)
color = colorScheme.onSurface.copy(alpha = 0.8f),
lineHeight = 18.sp
)
) )
if (category.type == PermissionType.PRECISE_LOCATION) { if (category.type == PermissionType.PRECISE_LOCATION) {
// Extra emphasis for location permission // Extra emphasis for location permission
Spacer(modifier = Modifier.height(4.dp))
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp) horizontalArrangement = Arrangement.spacedBy(6.dp)
) { ) {
Text( Icon(
text = "⚠️", imageVector = Icons.Filled.Warning,
style = MaterialTheme.typography.bodyMedium, contentDescription = "Warning",
tint = Color(0xFFFF9800),
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Text( Text(
@@ -233,22 +235,12 @@ private fun PermissionCategoryCard(
} }
} }
private fun getPermissionEmoji(permissionType: PermissionType): String { private fun getPermissionIcon(permissionType: PermissionType): ImageVector {
return when (permissionType) { return when (permissionType) {
PermissionType.NEARBY_DEVICES -> "📱" PermissionType.NEARBY_DEVICES -> Icons.Filled.Bluetooth
PermissionType.PRECISE_LOCATION -> "📍" PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn
PermissionType.NOTIFICATIONS -> "🔔" PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications
PermissionType.BATTERY_OPTIMIZATION -> "🔋" PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power
PermissionType.OTHER -> "🔧" PermissionType.OTHER -> Icons.Filled.Settings
}
}
private fun getPermissionIconColor(permissionType: PermissionType): Color {
return when (permissionType) {
PermissionType.NEARBY_DEVICES -> Color(0xFF2196F3) // Blue
PermissionType.PRECISE_LOCATION -> Color(0xFFFF9800) // Orange
PermissionType.NOTIFICATIONS -> Color(0xFF4CAF50) // Green
PermissionType.BATTERY_OPTIMIZATION -> Color(0xFFF44336) // Red
PermissionType.OTHER -> Color(0xFF9C27B0) // Purple
} }
} }
@@ -3,6 +3,8 @@ package com.bitchat.android.ui
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
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
@@ -73,12 +75,21 @@ fun LocationChannelsSheet(
// Bottom sheet state // Bottom sheet state
val sheetState = rememberModalBottomSheetState( val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = isInputFocused skipPartiallyExpanded = true
) )
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
// Scroll state for LazyColumn // Scroll state for LazyColumn with animated top bar
val listState = rememberLazyListState() val listState = rememberLazyListState()
val isScrolled by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
}
}
val topBarAlpha by animateFloatAsState(
targetValue = if (isScrolled) 0.95f else 0f,
label = "topBarAlpha"
)
val mapPickerLauncher = rememberLauncherForActivityResult( val mapPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
@@ -100,39 +111,54 @@ fun LocationChannelsSheet(
if (isPresented) { if (isPresented) {
ModalBottomSheet( ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
sheetState = sheetState, sheetState = sheetState,
modifier = modifier containerColor = MaterialTheme.colorScheme.background,
dragHandle = null
) { ) {
Box(modifier = Modifier.fillMaxWidth()) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = 48.dp, bottom = 16.dp)
) {
// Header Section
item(key = "header") {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.then( .padding(horizontal = 24.dp)
if (isInputFocused) { .padding(bottom = 8.dp),
Modifier.fillMaxHeight().padding(horizontal = 16.dp, vertical = 24.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
} else {
Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
}
),
verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
// Header
Text( Text(
text = "#location channels", text = "#location channels",
fontSize = 18.sp, style = MaterialTheme.typography.headlineSmall,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
) )
Text( Text(
text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy.", text = "chat with people near you using geohash channels. only a coarse geohash is shared, never exact gps. do not screenshot or share this screen to protect your privacy.",
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)
) )
}
}
// Permission controls if services enabled // Permission controls if services enabled
if (locationServicesEnabled) { if (locationServicesEnabled) {
item(key = "permissions") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
when (permissionState) { when (permissionState) {
LocationChannelManager.PermissionState.NOT_DETERMINED -> { LocationChannelManager.PermissionState.NOT_DETERMINED -> {
Button( Button(
@@ -152,7 +178,7 @@ fun LocationChannelsSheet(
} }
LocationChannelManager.PermissionState.DENIED, LocationChannelManager.PermissionState.DENIED,
LocationChannelManager.PermissionState.RESTRICTED -> { LocationChannelManager.PermissionState.RESTRICTED -> {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text( Text(
text = "location permission denied. enable in settings to use location channels.", text = "location permission denied. enable in settings to use location channels.",
fontSize = 11.sp, fontSize = 11.sp,
@@ -185,7 +211,7 @@ fun LocationChannelsSheet(
} }
null -> { null -> {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator(modifier = Modifier.size(12.dp)) CircularProgressIndicator(modifier = Modifier.size(12.dp))
@@ -199,15 +225,11 @@ fun LocationChannelsSheet(
} }
} }
} }
}
}
// Channel list (iOS-style plain list)
LazyColumn(
state = listState,
modifier = Modifier.weight(1f)
) {
// Mesh option first // Mesh option first
item { item(key = "mesh") {
ChannelRow( ChannelRow(
title = meshTitleWithCount(viewModel), title = meshTitleWithCount(viewModel),
subtitle = "#bluetooth • ${bluetoothRangeString()}", subtitle = "#bluetooth • ${bluetoothRangeString()}",
@@ -259,7 +281,7 @@ fun LocationChannelsSheet(
} else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) { } else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) {
item { item {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
CircularProgressIndicator(modifier = Modifier.size(16.dp)) CircularProgressIndicator(modifier = Modifier.size(16.dp))
@@ -274,13 +296,16 @@ fun LocationChannelsSheet(
// Bookmarked geohashes // Bookmarked geohashes
if (bookmarks.isNotEmpty()) { if (bookmarks.isNotEmpty()) {
item { item(key = "bookmarked_header") {
Text( Text(
text = "bookmarked", text = "bookmarked",
fontSize = 12.sp, style = MaterialTheme.typography.labelLarge,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 8.dp, bottom = 4.dp)
) )
} }
items(bookmarks) { gh -> items(bookmarks) { gh ->
@@ -325,16 +350,19 @@ fun LocationChannelsSheet(
} }
// Custom geohash teleport (iOS-style inline form) // Custom geohash teleport (iOS-style inline form)
item { item(key = "custom_geohash") {
Surface( Surface(
color = Color.Transparent,
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 2.dp)
) {
Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 6.dp), .padding(horizontal = 16.dp, vertical = 6.dp),
color = Color.Transparent horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
Row(
horizontalArrangement = Arrangement.spacedBy(1.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
@@ -435,7 +463,7 @@ fun LocationChannelsSheet(
) )
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
@@ -453,21 +481,32 @@ fun LocationChannelsSheet(
} }
} }
} }
}
}
customError?.let { error -> // Error message for custom geohash
if (customError != null) {
item(key = "geohash_error") {
Text( Text(
text = error, text = customError!!,
fontSize = 12.sp, fontSize = 12.sp,
fontFamily = FontFamily.Monospace, fontFamily = FontFamily.Monospace,
color = Color.Red color = Color.Red,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
) )
} }
} }
}
}
// Location services toggle button // Location services toggle button
item { item(key = "location_toggle") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(top = 8.dp)
) {
Button( Button(
onClick = { onClick = {
if (locationServicesEnabled) { if (locationServicesEnabled) {
@@ -503,6 +542,29 @@ fun LocationChannelsSheet(
} }
} }
} }
// TopBar (animated)
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.fillMaxWidth()
.height(56.dp)
.background(MaterialTheme.colorScheme.background.copy(alpha = topBarAlpha))
) {
TextButton(
onClick = onDismiss,
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(horizontal = 16.dp)
) {
Text(
text = "Close",
style = MaterialTheme.typography.labelMedium.copy(fontWeight = FontWeight.Bold),
color = MaterialTheme.colorScheme.onBackground
)
}
}
}
} }
} }
@@ -557,18 +619,20 @@ private fun ChannelRow(
Color.Transparent Color.Transparent
}, },
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
modifier = Modifier.fillMaxWidth() modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 2.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = 16.dp, vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(2.dp)
) { ) {
// Split title to handle count part with smaller font (iOS style) // Split title to handle count part with smaller font (iOS style)
val (baseTitle, countSuffix) = splitTitleAndCount(title) val (baseTitle, countSuffix) = splitTitleAndCount(title)