A cheap attempt at a native Bluesky client for Android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

LoginView: make more user-friendly

geesawra 0e810e34 0abc9b84

+70 -39
+70 -39
app/src/main/java/industries/geesawra/jerryno/LoginView.kt
··· 4 4 import android.widget.Toast 5 5 import androidx.compose.foundation.layout.Column 6 6 import androidx.compose.foundation.layout.fillMaxWidth 7 + import androidx.compose.foundation.layout.imePadding 8 + import androidx.compose.foundation.layout.navigationBarsPadding 7 9 import androidx.compose.foundation.layout.padding 8 10 import androidx.compose.foundation.text.KeyboardOptions 9 11 import androidx.compose.material.icons.Icons ··· 16 18 import androidx.compose.material3.Text 17 19 import androidx.compose.material3.TextField 18 20 import androidx.compose.runtime.Composable 21 + import androidx.compose.runtime.DisposableEffect 19 22 import androidx.compose.runtime.getValue 20 23 import androidx.compose.runtime.mutableStateOf 21 24 import androidx.compose.runtime.remember 22 25 import androidx.compose.runtime.rememberCoroutineScope 26 + import androidx.compose.runtime.rememberUpdatedState 23 27 import androidx.compose.runtime.setValue 24 28 import androidx.compose.ui.Alignment 25 29 import androidx.compose.ui.Modifier ··· 31 35 import androidx.compose.ui.text.input.VisualTransformation 32 36 import androidx.compose.ui.unit.dp 33 37 import industries.geesawra.jerryno.datalayer.BlueskyConn 38 + import kotlinx.coroutines.CoroutineScope 39 + import kotlinx.coroutines.delay 34 40 import kotlinx.coroutines.launch 35 41 import sh.christian.ozone.api.Handle 36 42 ··· 39 45 modifier: Modifier = Modifier, 40 46 navigate: () -> Unit 41 47 ) { 42 - var handle by remember { mutableStateOf("") } 43 48 var password by remember { mutableStateOf("") } 44 49 var isPasswordFocused by remember { mutableStateOf(false) } 45 50 var passwordVisible by remember { mutableStateOf(false) } ··· 49 54 val appPasswordRegex = "[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}-[a-zA-Z0-9]{4}".toRegex() 50 55 var currentPDS by remember { mutableStateOf("") } 51 56 var lookingUpPDS by remember { mutableStateOf(false) } 57 + val handleTextFieldError = remember { mutableStateOf(false) } 58 + val loggingIn = remember { mutableStateOf(false) } 59 + var handle by remember { mutableStateOf("") } 60 + handle.useDebounce { it, scope -> 61 + val handle = it 62 + if (handle.isEmpty()) { 63 + return@useDebounce 64 + } 65 + 66 + if (!handle.isATHandle()) { 67 + return@useDebounce 68 + } 69 + 70 + scope.launch { 71 + lookingUpPDS = true 72 + currentPDS = BlueskyConn.pdsForHandle(handle).onSuccess { 73 + handleTextFieldError.value = false 74 + Result.success("") 75 + }.onFailure { 76 + Toast.makeText( 77 + ctx, 78 + it.message ?: "Can't look up PDS: ${it.toString()}", 79 + Toast.LENGTH_LONG 80 + ).show() 81 + handleTextFieldError.value = true 82 + it 83 + }.getOrDefault("") 84 + lookingUpPDS = false 85 + } 86 + 87 + } 52 88 53 89 Column( 54 90 modifier = modifier 55 91 .fillMaxWidth() 56 - .padding(start = 8.dp, end = 8.dp), 92 + .padding(start = 8.dp, end = 8.dp) 93 + .imePadding() 94 + .navigationBarsPadding(), 57 95 horizontalAlignment = Alignment.CenterHorizontally 58 96 ) { 59 97 Text( ··· 63 101 ) 64 102 TextField( 65 103 value = handle, 104 + isError = handleTextFieldError.value, 66 105 onValueChange = { handle = it }, 67 106 label = { Text("Handle (e.g., yourname.bsky.social)") }, 68 107 modifier = Modifier 69 - .fillMaxWidth() 70 - .onFocusChanged { focusState -> 71 - when (focusState.isFocused) { 72 - true -> { 73 - lookingUpPDS = false 74 - currentPDS = "" 75 - } 76 - 77 - false -> { 78 - if (handle.isEmpty()) { 79 - currentPDS = "" 80 - return@onFocusChanged 81 - } 82 - 83 - if (currentPDS != "") { 84 - return@onFocusChanged 85 - } 86 - 87 - scope.launch { 88 - lookingUpPDS = true 89 - currentPDS = BlueskyConn.pdsForHandle(handle).getOrElse { 90 - Toast.makeText( 91 - ctx, 92 - it.message ?: "Error: ${it.toString()}", 93 - Toast.LENGTH_LONG 94 - ) 95 - .show() 96 - lookingUpPDS = false 97 - return@launch 98 - } 99 - lookingUpPDS = false 100 - } 101 - } 102 - } 103 - }, 108 + .fillMaxWidth(), 104 109 keyboardOptions = KeyboardOptions( 105 110 keyboardType = KeyboardType.Email, 106 111 capitalization = KeyboardCapitalization.None, ··· 133 138 Button( 134 139 onClick = { 135 140 scope.launch { 141 + loggingIn.value = true 136 142 bc.login(currentPDS, handle, password).onSuccess { 137 143 navigate() 138 144 }.onFailure { ··· 140 146 Toast.makeText(ctx, it.message ?: "Unknown login error", Toast.LENGTH_LONG) 141 147 .show() 142 148 } 149 + loggingIn.value = false 143 150 } 144 151 }, 145 - enabled = (Handle.Regex.matches(handle.removePrefix("@"))) && password.isNotEmpty() && currentPDS.isNotEmpty(), 152 + enabled = handle.isATHandle() && password.isNotEmpty() && currentPDS.isNotEmpty() && !loggingIn.value, 146 153 modifier = Modifier.padding(top = 8.dp, bottom = 8.dp) 147 154 ) { 148 155 Text("Login") ··· 173 180 ) 174 181 } 175 182 } 176 - } 183 + } 184 + 185 + fun String.isATHandle(): Boolean { 186 + return Handle.Regex.matches(this.removePrefix("@")) 187 + } 188 + 189 + @Composable 190 + fun <T> T.useDebounce( 191 + delayMillis: Long = 300L, 192 + coroutineScope: CoroutineScope = rememberCoroutineScope(), 193 + onChange: (T, CoroutineScope) -> Unit 194 + ): T { 195 + val state by rememberUpdatedState(this) 196 + 197 + DisposableEffect(state) { 198 + val job = coroutineScope.launch { 199 + delay(delayMillis) 200 + onChange(state, coroutineScope) 201 + } 202 + onDispose { 203 + job.cancel() 204 + } 205 + } 206 + return state 207 + }