A cheap attempt at a native Bluesky client for Android

ComposeView: Add close button and improve state handling

Add an explicit "Close" button to the compose view sheet. This allows users to dismiss the sheet without sending a post.

When the sheet is closed, its state is now properly reset. This includes clearing any entered text, selected media, and reply/quote context. Additionally, the keyboard is hidden and focus is cleared when the sheet is hidden or partially expanded.

The Proguard configuration was also updated to no longer keep all of Ktor's classes, reducing the final application size.

+44 -25
+11
.idea/deploymentTargetSelector.xml
··· 13 13 </DropdownSelection> 14 14 <DialogSelection /> 15 15 </SelectionState> 16 + <SelectionState runConfigName="app (release)"> 17 + <option name="selectionMode" value="DROPDOWN" /> 18 + <DropdownSelection timestamp="2025-10-13T14:56:43.774060Z"> 19 + <Target type="DEFAULT_BOOT"> 20 + <handle> 21 + <DeviceId pluginId="LocalEmulator" identifier="path=/Users/gsora/.android/avd/Medium_Phone.avd" /> 22 + </handle> 23 + </Target> 24 + </DropdownSelection> 25 + <DialogSelection /> 26 + </SelectionState> 16 27 </selectionStates> 17 28 </component> 18 29 </project>
-1
app/proguard-rules.pro
··· 19 19 # If you keep the line number information, uncomment this to 20 20 # hide the original source file name. 21 21 #-renamesourcefileattribute SourceFile 22 - -keep class io.ktor.** { *; } 23 22 -dontwarn io.ktor.** 24 23 # Please add these rules to your existing keep rules in order to suppress warnings. 25 24 # This is generated automatically by the Android Gradle plugin.
+31 -20
app/src/main/java/industries/geesawra/monarch/ComposeView.kt
··· 38 38 import androidx.compose.material.icons.Icons 39 39 import androidx.compose.material.icons.automirrored.filled.Send 40 40 import androidx.compose.material.icons.filled.CameraRoll 41 + import androidx.compose.material.icons.filled.Close 41 42 import androidx.compose.material3.BottomSheetScaffoldState 42 43 import androidx.compose.material3.Button 43 44 import androidx.compose.material3.ButtonDefaults ··· 48 49 import androidx.compose.material3.MaterialTheme 49 50 import androidx.compose.material3.OutlinedCard 50 51 import androidx.compose.material3.OutlinedTextField 52 + import androidx.compose.material3.SheetValue 51 53 import androidx.compose.material3.Text 52 54 import androidx.compose.material3.TextButton 53 55 import androidx.compose.runtime.Composable ··· 61 63 import androidx.compose.ui.focus.FocusRequester 62 64 import androidx.compose.ui.focus.focusRequester 63 65 import androidx.compose.ui.graphics.Color 66 + import androidx.compose.ui.platform.LocalFocusManager 64 67 import androidx.compose.ui.platform.LocalSoftwareKeyboardController 65 68 import androidx.compose.ui.unit.dp 66 69 import com.atproto.repo.StrongRef ··· 81 84 scrollState: ScrollState 82 85 ) { 83 86 val focusRequester = remember { FocusRequester() } 87 + val focusManager = LocalFocusManager.current 84 88 val keyboardController = LocalSoftwareKeyboardController.current 85 89 val charCount = remember { mutableIntStateOf(0) } 86 90 val wasEdited = remember { mutableStateOf(false) } ··· 91 95 val mediaSelected = remember { mutableStateOf(listOf<Uri>()) } 92 96 val mediaSelectedIsVideo = remember { mutableStateOf(false) } 93 97 94 - LaunchedEffect(scaffoldState.bottomSheetState.isVisible) { 95 - if (scaffoldState.bottomSheetState.isVisible) { 96 - keyboardController?.show() 97 - focusRequester.requestFocus() 98 - } else { 99 - keyboardController?.hide() 100 - // Reset state when sheet is hidden 101 - composeFieldState.clearText() 102 - charCount.intValue = 0 103 - inReplyTo.value = null 104 - isQuotePost.value = false 105 - mediaSelected.value = listOf() 106 - mediaSelectedIsVideo.value = false 98 + LaunchedEffect(scaffoldState.bottomSheetState.currentValue) { 99 + when (scaffoldState.bottomSheetState.currentValue) { 100 + SheetValue.Hidden -> { 101 + composeFieldState.clearText() 102 + keyboardController?.hide() 103 + focusManager.clearFocus() 104 + charCount.intValue = 0 105 + inReplyTo.value = null 106 + isQuotePost.value = false 107 + mediaSelected.value = listOf() 108 + mediaSelectedIsVideo.value = false 109 + } 107 110 111 + SheetValue.PartiallyExpanded, SheetValue.Expanded -> { 112 + keyboardController?.hide() 113 + focusManager.clearFocus() 114 + } 108 115 } 109 116 } 110 117 ··· 183 190 Column( 184 191 modifier = Modifier 185 192 .fillMaxWidth(), // Takes full width of the Inner Box 186 - horizontalAlignment = Alignment.CenterHorizontally 193 + horizontalAlignment = Alignment.End 187 194 ) { 188 195 Row { 189 - Text( 190 - "New Post", 191 - style = MaterialTheme.typography.titleLarge, 192 - modifier = Modifier.padding(bottom = 16.dp) 193 - ) 196 + Button( 197 + onClick = { 198 + coroutineScope.launch { 199 + scaffoldState.bottomSheetState.hide() 200 + } 201 + } 202 + ) { 203 + Icon(Icons.Default.Close, contentDescription = "Close compose view") 204 + } 194 205 } 195 206 196 207 inReplyTo.value?.let { ··· 394 405 Text("Skeet") 395 406 } 396 407 } 397 - } 408 + }
+2 -4
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 20 20 import androidx.compose.material.icons.filled.Home 21 21 import androidx.compose.material.icons.filled.Notifications 22 22 import androidx.compose.material.icons.filled.Tag 23 - import androidx.compose.material3.BottomSheetDefaults 24 23 import androidx.compose.material3.BottomSheetScaffold 25 24 import androidx.compose.material3.DrawerValue 26 25 import androidx.compose.material3.ExperimentalMaterial3Api ··· 99 98 .windowInsetsPadding(WindowInsets.statusBars), 100 99 scaffoldState = scaffoldState, 101 100 sheetPeekHeight = 0.dp, 102 - sheetDragHandle = { 103 - BottomSheetDefaults.DragHandle() 104 - }, 101 + sheetDragHandle = {}, 102 + sheetSwipeEnabled = false, 105 103 sheetContent = { 106 104 ComposeView( 107 105 context = LocalContext.current,