diff --git a/app/src/main/java/com/bitchat/android/MainActivity.kt b/app/src/main/java/com/bitchat/android/MainActivity.kt index 9ae550b1..f35a403c 100644 --- a/app/src/main/java/com/bitchat/android/MainActivity.kt +++ b/app/src/main/java/com/bitchat/android/MainActivity.kt @@ -25,6 +25,7 @@ import com.bitchat.android.onboarding.BluetoothCheckScreen import com.bitchat.android.onboarding.BluetoothStatus import com.bitchat.android.onboarding.BluetoothStatusManager import com.bitchat.android.onboarding.BatteryOptimizationManager +import com.bitchat.android.onboarding.BatteryOptimizationPreferenceManager import com.bitchat.android.onboarding.BatteryOptimizationScreen import com.bitchat.android.onboarding.BatteryOptimizationStatus import com.bitchat.android.onboarding.InitializationErrorScreen @@ -163,7 +164,7 @@ class MainActivity : ComponentActivity() { } when (onboardingState) { - OnboardingState.CHECKING -> { + OnboardingState.PERMISSION_REQUESTING -> { InitializingScreen(modifier) } @@ -226,16 +227,8 @@ class MainActivity : ComponentActivity() { } ) } - - OnboardingState.PERMISSION_REQUESTING -> { - InitializingScreen(modifier) - } - - OnboardingState.INITIALIZING -> { - InitializingScreen(modifier) - } - - OnboardingState.COMPLETE -> { + + OnboardingState.CHECKING, OnboardingState.INITIALIZING, OnboardingState.COMPLETE -> { // Set up back navigation handling for the chat screen val backCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -533,6 +526,13 @@ class MainActivity : ComponentActivity() { 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 batteryOptimizationManager.logBatteryOptimizationStatus() val currentBatteryOptimizationStatus = when { diff --git a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationPreferenceManager.kt b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationPreferenceManager.kt new file mode 100644 index 00000000..c749dc43 --- /dev/null +++ b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationPreferenceManager.kt @@ -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 = _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) + } +} diff --git a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt index a026d47e..4ef455f2 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/BatteryOptimizationScreen.kt @@ -3,6 +3,7 @@ package com.bitchat.android.onboarding import androidx.compose.animation.core.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* @@ -12,11 +13,13 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.bitchat.android.R /** @@ -32,10 +35,16 @@ fun BatteryOptimizationScreen( onSkip: () -> Unit, isLoading: Boolean = false ) { + val context = LocalContext.current val colorScheme = MaterialTheme.colorScheme + + // Initialize preference manager + LaunchedEffect(Unit) { + BatteryOptimizationPreferenceManager.init(context) + } Box( - modifier = modifier.padding(32.dp), + modifier = modifier.padding(24.dp), contentAlignment = Alignment.Center ) { when (status) { @@ -73,6 +82,8 @@ private fun BatteryOptimizationEnabledContent( colorScheme: ColorScheme, isLoading: Boolean ) { + val context = LocalContext.current + Column( modifier = Modifier.fillMaxSize() ) { @@ -82,78 +93,112 @@ private fun BatteryOptimizationEnabledContent( .weight(1f) .verticalScroll(rememberScrollState()) .padding(bottom = 16.dp), - verticalArrangement = Arrangement.spacedBy(24.dp), - horizontalAlignment = Alignment.CenterHorizontally + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = "bitchat", - style = MaterialTheme.typography.headlineLarge.copy( + // Header Section - matching AboutSheet style + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "bitchat", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + fontSize = 32.sp + ), + color = colorScheme.onBackground + ) + + Text( + text = "battery optimization detected", + fontSize = 12.sp, fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Bold, - color = colorScheme.primary + color = colorScheme.onBackground.copy(alpha = 0.7f) ) - ) + } - Spacer(modifier = Modifier.height(16.dp)) - - Icon( - imageVector = Icons.Outlined.BatteryAlert, - contentDescription = "Battery Optimization", - modifier = Modifier.size(64.dp), - tint = colorScheme.error - ) - - Text( - text = stringResource(R.string.battery_optimization_detected), - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - color = colorScheme.onSurface - ), - textAlign = TextAlign.Center - ) - - Text( - text = stringResource(R.string.battery_optimization_explanation), - style = MaterialTheme.typography.bodyLarge.copy( - color = colorScheme.onSurfaceVariant - ), - textAlign = TextAlign.Center - ) - - Card( + // Battery optimization info section + Surface( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = colorScheme.surfaceVariant.copy(alpha = 0.5f) - ) + color = colorScheme.surfaceVariant.copy(alpha = 0.25f), + shape = RoundedCornerShape(12.dp) ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( - text = stringResource(R.string.battery_optimization_why_disable), - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.SemiBold, - color = colorScheme.onSurface + 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) ) - ) - - Text( - text = stringResource(R.string.battery_optimization_benefits), - style = MaterialTheme.typography.bodyMedium.copy( - color = colorScheme.onSurfaceVariant - ) - ) + Column { + Text( + text = "Battery Optimization Enabled", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "bitchat needs to run in the background to maintain mesh connections. battery optimization can interrupt these connections.", + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackground.copy(alpha = 0.8f) + ) + } + } } } - Text( - text = stringResource(R.string.battery_optimization_note), - style = MaterialTheme.typography.bodySmall.copy( - color = colorScheme.onSurfaceVariant - ), - textAlign = TextAlign.Center - ) + // Benefits section + Surface( + modifier = Modifier.fillMaxWidth(), + color = colorScheme.surfaceVariant.copy(alpha = 0.25f), + shape = RoundedCornerShape(12.dp) + ) { + 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 @@ -164,7 +209,10 @@ private fun BatteryOptimizationEnabledContent( Button( onClick = onDisableBatteryOptimization, modifier = Modifier.fillMaxWidth(), - enabled = !isLoading + enabled = !isLoading, + colors = ButtonDefaults.buttonColors( + containerColor = colorScheme.primary + ) ) { if (isLoading) { CircularProgressIndicator( @@ -174,7 +222,13 @@ private fun BatteryOptimizationEnabledContent( ) 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( @@ -186,15 +240,28 @@ private fun BatteryOptimizationEnabledContent( modifier = Modifier.weight(1f), enabled = !isLoading ) { - Text(stringResource(R.string.battery_optimization_check_again)) + Text( + text = "Check Again", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace + ) + ) } TextButton( - onClick = onSkip, + onClick = { + BatteryOptimizationPreferenceManager.setSkipped(context, true) + onSkip() + }, modifier = Modifier.weight(1f), enabled = !isLoading ) { - Text(stringResource(R.string.battery_optimization_skip)) + Text( + text = "Skip for Now", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace + ) + ) } } } @@ -206,17 +273,31 @@ private fun BatteryOptimizationCheckingContent( colorScheme: ColorScheme ) { Column( - verticalArrangement = Arrangement.spacedBy(32.dp), + verticalArrangement = Arrangement.spacedBy(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = "bitchat", - style = MaterialTheme.typography.headlineLarge.copy( - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Bold, - color = colorScheme.primary + // Header Section - matching AboutSheet style + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "bitchat", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + 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 rotation by infiniteTransition.animateFloat( @@ -239,18 +320,10 @@ private fun BatteryOptimizationCheckingContent( ) Text( - text = stringResource(R.string.battery_optimization_disabled), - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - color = colorScheme.onSurface - ), - textAlign = TextAlign.Center - ) - - Text( - text = stringResource(R.string.battery_optimization_success_message), - style = MaterialTheme.typography.bodyLarge.copy( - color = colorScheme.onSurfaceVariant + text = "bitchat can run reliably in the background", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onBackground.copy(alpha = 0.8f) ), textAlign = TextAlign.Center ) @@ -266,14 +339,28 @@ private fun BatteryOptimizationNotSupportedContent( verticalArrangement = Arrangement.spacedBy(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text( - text = "bitchat", - style = MaterialTheme.typography.headlineLarge.copy( - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Bold, - color = colorScheme.primary + // Header Section - matching AboutSheet style + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "bitchat", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + 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( imageVector = Icons.Filled.CheckCircle, @@ -283,27 +370,28 @@ private fun BatteryOptimizationNotSupportedContent( ) Text( - text = stringResource(R.string.battery_optimization_not_required), - style = MaterialTheme.typography.headlineSmall.copy( - fontWeight = FontWeight.SemiBold, - color = colorScheme.onSurface - ), - textAlign = TextAlign.Center - ) - - Text( - text = stringResource(R.string.battery_optimization_not_supported_message), - style = MaterialTheme.typography.bodyLarge.copy( - color = colorScheme.onSurfaceVariant + text = "your device doesn't require battery optimization settings. bitchat will run normally.", + style = MaterialTheme.typography.bodyMedium.copy( + fontFamily = FontFamily.Monospace, + color = colorScheme.onBackground.copy(alpha = 0.8f) ), textAlign = TextAlign.Center ) Button( 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 + ) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt index a925567e..997637f3 100644 --- a/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt +++ b/app/src/main/java/com/bitchat/android/onboarding/PermissionExplanationScreen.kt @@ -2,12 +2,22 @@ package com.bitchat.android.onboarding import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape 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.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.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -40,86 +50,87 @@ fun PermissionExplanationScreen( verticalArrangement = Arrangement.spacedBy(16.dp) ) { Spacer(modifier = Modifier.height(24.dp)) - // Header + + // Header Section - matching AboutSheet style Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Bottom, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "bitchat", + style = MaterialTheme.typography.headlineLarge.copy( + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + fontSize = 32.sp + ), + color = colorScheme.onBackground + ) + } + Text( - text = "Welcome to bitchat", - style = MaterialTheme.typography.headlineMedium.copy( - fontFamily = FontFamily.Monospace, - fontWeight = FontWeight.Bold, - color = colorScheme.primary - ), - textAlign = TextAlign.Center - ) - - 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 + text = "decentralized mesh messaging with end-to-end encryption", + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + color = colorScheme.onBackground.copy(alpha = 0.7f) ) } - Spacer(modifier = Modifier.height(16.dp)) - - // Privacy assurance section - Card( + // Privacy assurance section - matching AboutSheet card style + Surface( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + color = colorScheme.surfaceVariant.copy(alpha = 0.25f), + shape = RoundedCornerShape(12.dp) ) { Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - Text( - text = "šŸ”’", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.size(20.dp) + Icon( + imageVector = Icons.Filled.Security, + contentDescription = "Privacy Protected", + tint = colorScheme.primary, + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp) ) - Text( - text = "Your Privacy is Protected", - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - color = colorScheme.onSurface + Column { + Text( + text = "Your Privacy is Protected", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + 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) + ) + } } - - 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)) - + // Section header Text( - text = "To work properly, bitchat needs these permissions:", - style = MaterialTheme.typography.bodyMedium.copy( - fontWeight = FontWeight.Medium, - color = colorScheme.onSurface - ) + text = "permissions", + style = MaterialTheme.typography.labelLarge, + color = colorScheme.onBackground.copy(alpha = 0.7f), + modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) ) // Permission categories @@ -168,55 +179,46 @@ private fun PermissionCategoryCard( category: PermissionCategory, colorScheme: ColorScheme ) { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + Row( + verticalAlignment = Alignment.Top, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp) ) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Text( - text = getPermissionEmoji(category.type), - style = MaterialTheme.typography.titleLarge, - color = getPermissionIconColor(category.type), - modifier = Modifier.size(24.dp) - ) - - Text( - text = category.type.nameValue, - style = MaterialTheme.typography.titleSmall.copy( - fontWeight = FontWeight.Bold, - color = colorScheme.onSurface - ) - ) - } - + Icon( + imageVector = getPermissionIcon(category.type), + contentDescription = category.type.nameValue, + tint = colorScheme.primary, + modifier = Modifier + .padding(top = 2.dp) + .size(20.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text( + text = category.type.nameValue, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = colorScheme.onBackground + ) + Spacer(modifier = Modifier.height(4.dp)) Text( text = category.description, - style = MaterialTheme.typography.bodySmall.copy( - fontFamily = FontFamily.Monospace, - color = colorScheme.onSurface.copy(alpha = 0.8f), - lineHeight = 18.sp - ) + style = MaterialTheme.typography.bodySmall, + color = colorScheme.onBackground.copy(alpha = 0.8f) ) if (category.type == PermissionType.PRECISE_LOCATION) { // Extra emphasis for location permission + Spacer(modifier = Modifier.height(4.dp)) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Text( - text = "āš ļø", - style = MaterialTheme.typography.bodyMedium, + Icon( + imageVector = Icons.Filled.Warning, + contentDescription = "Warning", + tint = Color(0xFFFF9800), modifier = Modifier.size(16.dp) ) Text( @@ -233,22 +235,12 @@ private fun PermissionCategoryCard( } } -private fun getPermissionEmoji(permissionType: PermissionType): String { +private fun getPermissionIcon(permissionType: PermissionType): ImageVector { return when (permissionType) { - PermissionType.NEARBY_DEVICES -> "šŸ“±" - PermissionType.PRECISE_LOCATION -> "šŸ“" - PermissionType.NOTIFICATIONS -> "šŸ””" - PermissionType.BATTERY_OPTIMIZATION -> "šŸ”‹" - PermissionType.OTHER -> "šŸ”§" - } -} - -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 + PermissionType.NEARBY_DEVICES -> Icons.Filled.Bluetooth + PermissionType.PRECISE_LOCATION -> Icons.Filled.LocationOn + PermissionType.NOTIFICATIONS -> Icons.Filled.Notifications + PermissionType.BATTERY_OPTIMIZATION -> Icons.Filled.Power + PermissionType.OTHER -> Icons.Filled.Settings } } diff --git a/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt b/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt index a9843d4b..fd8ad1eb 100644 --- a/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt +++ b/app/src/main/java/com/bitchat/android/ui/LocationChannelsSheet.kt @@ -3,6 +3,8 @@ package com.bitchat.android.ui import android.content.Intent import android.net.Uri import android.provider.Settings +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.items @@ -73,12 +75,21 @@ fun LocationChannelsSheet( // Bottom sheet state val sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = isInputFocused + skipPartiallyExpanded = true ) val coroutineScope = rememberCoroutineScope() - // Scroll state for LazyColumn + // Scroll state for LazyColumn with animated top bar 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( contract = ActivityResultContracts.StartActivityForResult() @@ -100,114 +111,125 @@ fun LocationChannelsSheet( if (isPresented) { ModalBottomSheet( + modifier = modifier.statusBarsPadding(), onDismissRequest = onDismiss, sheetState = sheetState, - modifier = modifier + containerColor = MaterialTheme.colorScheme.background, + dragHandle = null ) { - Column( - modifier = Modifier - .fillMaxWidth() - .then( - if (isInputFocused) { - Modifier.fillMaxHeight().padding(horizontal = 16.dp, vertical = 24.dp) - } else { - Modifier.padding(horizontal = 16.dp, vertical = 12.dp) + Box(modifier = Modifier.fillMaxWidth()) { + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(top = 48.dp, bottom = 16.dp) + ) { + // Header Section + item(key = "header") { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = "#location channels", + style = MaterialTheme.typography.headlineSmall, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onBackground + ) + + 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.", + fontSize = 12.sp, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f) + ) } - ), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - // Header - Text( - text = "#location channels", - fontSize = 18.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface - ) + } - 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.", - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) - - // Permission controls if services enabled - if (locationServicesEnabled) { - when (permissionState) { - LocationChannelManager.PermissionState.NOT_DETERMINED -> { - Button( - onClick = { locationManager.enableLocationChannels() }, - colors = ButtonDefaults.buttonColors( - containerColor = standardGreen.copy(alpha = 0.12f), - contentColor = standardGreen - ), - modifier = Modifier.fillMaxWidth() + // Permission controls if services enabled + if (locationServicesEnabled) { + item(key = "permissions") { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Text( - text = "grant location permission", - fontSize = 12.sp, - fontFamily = FontFamily.Monospace - ) - } - } - LocationChannelManager.PermissionState.DENIED, - LocationChannelManager.PermissionState.RESTRICTED -> { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - Text( - text = "location permission denied. enable in settings to use location channels.", - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - color = Color.Red.copy(alpha = 0.8f) - ) - TextButton( - onClick = { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { - data = Uri.fromParts("package", context.packageName, null) + when (permissionState) { + LocationChannelManager.PermissionState.NOT_DETERMINED -> { + Button( + onClick = { locationManager.enableLocationChannels() }, + colors = ButtonDefaults.buttonColors( + containerColor = standardGreen.copy(alpha = 0.12f), + contentColor = standardGreen + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "grant location permission", + fontSize = 12.sp, + fontFamily = FontFamily.Monospace + ) + } + } + LocationChannelManager.PermissionState.DENIED, + LocationChannelManager.PermissionState.RESTRICTED -> { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = "location permission denied. enable in settings to use location channels.", + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = Color.Red.copy(alpha = 0.8f) + ) + TextButton( + onClick = { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } + ) { + Text( + text = "open settings", + fontSize = 11.sp, + fontFamily = FontFamily.Monospace + ) + } + } + } + LocationChannelManager.PermissionState.AUTHORIZED -> { + Text( + text = "āœ“ location permission granted", + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = standardGreen + ) + } + null -> { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator(modifier = Modifier.size(12.dp)) + Text( + text = "checking permissions...", + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) } - context.startActivity(intent) } - ) { - Text( - text = "open settings", - fontSize = 11.sp, - fontFamily = FontFamily.Monospace - ) } } } - LocationChannelManager.PermissionState.AUTHORIZED -> { - Text( - text = "āœ“ location permission granted", - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - color = standardGreen - ) - } - null -> { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator(modifier = Modifier.size(12.dp)) - Text( - text = "checking permissions...", - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) - } - } } - } - // Channel list (iOS-style plain list) - - LazyColumn( - state = listState, - modifier = Modifier.weight(1f) - ) { // Mesh option first - item { + item(key = "mesh") { ChannelRow( title = meshTitleWithCount(viewModel), subtitle = "#bluetooth • ${bluetoothRangeString()}", @@ -259,7 +281,7 @@ fun LocationChannelsSheet( } else if (permissionState == LocationChannelManager.PermissionState.AUTHORIZED && locationServicesEnabled) { item { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { CircularProgressIndicator(modifier = Modifier.size(16.dp)) @@ -274,13 +296,16 @@ fun LocationChannelsSheet( // Bookmarked geohashes if (bookmarks.isNotEmpty()) { - item { + item(key = "bookmarked_header") { Text( text = "bookmarked", - fontSize = 12.sp, + style = MaterialTheme.typography.labelLarge, fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), - modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 8.dp, bottom = 4.dp) ) } items(bookmarks) { gh -> @@ -325,182 +350,219 @@ fun LocationChannelsSheet( } // Custom geohash teleport (iOS-style inline form) - item { + item(key = "custom_geohash") { Surface( + color = Color.Transparent, + shape = MaterialTheme.shapes.medium, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 6.dp), - color = Color.Transparent + .padding(horizontal = 24.dp, vertical = 2.dp) ) { - Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { - Row( - horizontalArrangement = Arrangement.spacedBy(1.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "#", + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 6.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "#", + fontSize = BASE_FONT_SIZE.sp, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + + BasicTextField( + value = customGeohash, + onValueChange = { newValue -> + // iOS-style geohash validation (base32 characters only) + val allowed = "0123456789bcdefghjkmnpqrstuvwxyz".toSet() + val filtered = newValue + .lowercase() + .replace("#", "") + .filter { it in allowed } + .take(12) + + customGeohash = filtered + customError = null + }, + textStyle = androidx.compose.ui.text.TextStyle( fontSize = BASE_FONT_SIZE.sp, fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) - ) - - BasicTextField( - value = customGeohash, - onValueChange = { newValue -> - // iOS-style geohash validation (base32 characters only) - val allowed = "0123456789bcdefghjkmnpqrstuvwxyz".toSet() - val filtered = newValue - .lowercase() - .replace("#", "") - .filter { it in allowed } - .take(12) - - customGeohash = filtered - customError = null - }, - textStyle = androidx.compose.ui.text.TextStyle( - fontSize = BASE_FONT_SIZE.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface - ), - modifier = Modifier - .weight(1f) - .onFocusChanged { focusState -> - isInputFocused = focusState.isFocused - if (focusState.isFocused) { - coroutineScope.launch { - sheetState.expand() - // Scroll to bottom to show input and remove button - listState.animateScrollToItem( - index = listState.layoutInfo.totalItemsCount - 1 - ) - } + color = MaterialTheme.colorScheme.onSurface + ), + modifier = Modifier + .weight(1f) + .onFocusChanged { focusState -> + isInputFocused = focusState.isFocused + if (focusState.isFocused) { + coroutineScope.launch { + sheetState.expand() + // Scroll to bottom to show input and remove button + listState.animateScrollToItem( + index = listState.layoutInfo.totalItemsCount - 1 + ) } - }, - singleLine = true, - decorationBox = { innerTextField -> - if (customGeohash.isEmpty()) { - Text( - text = "geohash", - fontSize = BASE_FONT_SIZE.sp, - fontFamily = FontFamily.Monospace, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) - ) - } - innerTextField() - } - ) - - val normalized = customGeohash.trim().lowercase().replace("#", "") - - // Map picker button - IconButton(onClick = { - val initial = when { - normalized.isNotBlank() -> normalized - selectedChannel is ChannelID.Location -> (selectedChannel as ChannelID.Location).channel.geohash - else -> "" - } - val intent = Intent(context, GeohashPickerActivity::class.java).apply { - putExtra(GeohashPickerActivity.EXTRA_INITIAL_GEOHASH, initial) - } - mapPickerLauncher.launch(intent) - }) { - Icon( - imageVector = Icons.Filled.Map, - contentDescription = "Open map", - tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) - ) - } - - val isValid = validateGeohash(normalized) - - // iOS-style teleport button - Button( - onClick = { - if (isValid) { - val level = levelForLength(normalized.length) - val channel = GeohashChannel(level = level, geohash = normalized) - // Mark this selection as a manual teleport - locationManager.setTeleported(true) - locationManager.select(ChannelID.Location(channel)) - onDismiss() - } else { - customError = "invalid geohash" } }, - enabled = isValid, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f), - contentColor = MaterialTheme.colorScheme.onSurface - ) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically - ) { + singleLine = true, + decorationBox = { innerTextField -> + if (customGeohash.isEmpty()) { Text( - text = "teleport", + text = "geohash", fontSize = BASE_FONT_SIZE.sp, - fontFamily = FontFamily.Monospace - ) - // iOS has a face.dashed icon, use closest Material equivalent - Icon( - imageVector = Icons.Filled.PinDrop, - contentDescription = "Teleport", - modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.onSurface + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) ) } + innerTextField() } + ) + + val normalized = customGeohash.trim().lowercase().replace("#", "") + + // Map picker button + IconButton(onClick = { + val initial = when { + normalized.isNotBlank() -> normalized + selectedChannel is ChannelID.Location -> (selectedChannel as ChannelID.Location).channel.geohash + else -> "" + } + val intent = Intent(context, GeohashPickerActivity::class.java).apply { + putExtra(GeohashPickerActivity.EXTRA_INITIAL_GEOHASH, initial) + } + mapPickerLauncher.launch(intent) + }) { + Icon( + imageVector = Icons.Filled.Map, + contentDescription = "Open map", + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + ) } - customError?.let { error -> - Text( - text = error, - fontSize = 12.sp, - fontFamily = FontFamily.Monospace, - color = Color.Red + val isValid = validateGeohash(normalized) + + // iOS-style teleport button + Button( + onClick = { + if (isValid) { + val level = levelForLength(normalized.length) + val channel = GeohashChannel(level = level, geohash = normalized) + // Mark this selection as a manual teleport + locationManager.setTeleported(true) + locationManager.select(ChannelID.Location(channel)) + onDismiss() + } else { + customError = "invalid geohash" + } + }, + enabled = isValid, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.12f), + contentColor = MaterialTheme.colorScheme.onSurface ) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "teleport", + fontSize = BASE_FONT_SIZE.sp, + fontFamily = FontFamily.Monospace + ) + // iOS has a face.dashed icon, use closest Material equivalent + Icon( + imageVector = Icons.Filled.PinDrop, + contentDescription = "Teleport", + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurface + ) + } } } } } - // Location services toggle button - item { - Button( - onClick = { - if (locationServicesEnabled) { - locationManager.disableLocationServices() - } else { - locationManager.enableLocationServices() - } - }, - colors = ButtonDefaults.buttonColors( - containerColor = if (locationServicesEnabled) { - Color.Red.copy(alpha = 0.08f) - } else { - standardGreen.copy(alpha = 0.12f) - }, - contentColor = if (locationServicesEnabled) { - Color(0xFFBF1A1A) - } else { - standardGreen - } - ), - modifier = Modifier.fillMaxWidth() - ) { + // Error message for custom geohash + if (customError != null) { + item(key = "geohash_error") { Text( - text = if (locationServicesEnabled) { - "disable location services" - } else { - "enable location services" - }, + text = customError!!, fontSize = 12.sp, - fontFamily = FontFamily.Monospace + fontFamily = FontFamily.Monospace, + color = Color.Red, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) ) } } + + // Location services toggle button + item(key = "location_toggle") { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .padding(top = 8.dp) + ) { + Button( + onClick = { + if (locationServicesEnabled) { + locationManager.disableLocationServices() + } else { + locationManager.enableLocationServices() + } + }, + colors = ButtonDefaults.buttonColors( + containerColor = if (locationServicesEnabled) { + Color.Red.copy(alpha = 0.08f) + } else { + standardGreen.copy(alpha = 0.12f) + }, + contentColor = if (locationServicesEnabled) { + Color(0xFFBF1A1A) + } else { + standardGreen + } + ), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = if (locationServicesEnabled) { + "disable location services" + } else { + "enable location services" + }, + fontSize = 12.sp, + fontFamily = FontFamily.Monospace + ) + } + } + } + } + + // 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 }, shape = MaterialTheme.shapes.medium, - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 2.dp) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding(horizontal = 16.dp, vertical = 6.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Column( 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) val (baseTitle, countSuffix) = splitTitleAndCount(title)