A cheap attempt at a native Bluesky client for Android

*: make refresh experience more linear

+36 -28
+19 -7
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 194 194 isRefreshing.value = true 195 195 when (currentDestination) { 196 196 TabBarDestinations.TIMELINE -> { 197 - timelineViewModel.fetchTimeline { 198 - isRefreshing.value = false 197 + timelineViewModel.fetchTimeline(fresh = true) { 198 + coroutineScope.launch { 199 + isRefreshing.value = false 200 + timelineState.scrollToItem(0) 201 + } 199 202 } 200 203 } 201 204 202 205 TabBarDestinations.NOTIFICATIONS -> { 203 - timelineViewModel.fetchNotifications { 204 - isRefreshing.value = false 206 + timelineViewModel.fetchNotifications(fresh = true) { 207 + coroutineScope.launch { 208 + isRefreshing.value = false 209 + timelineState.scrollToItem(0) 210 + } 205 211 } 206 212 } 207 213 } ··· 218 224 uri, 219 225 displayName, 220 226 avatar 221 - ) { isRefreshing.value = false } 227 + ) { 228 + coroutineScope.launch { 229 + isRefreshing.value = false 230 + timelineState.scrollToItem(0) 231 + } 232 + } 233 + 222 234 coroutineScope.launch { 223 235 drawerState.close() 224 236 } ··· 366 378 state = timelineState, 367 379 modifier = Modifier.padding(values), 368 380 onReplyTap = onReplyTap, 369 - isScrollEnabled = remember { mutableStateOf(isScrollEnabled) } 381 + isScrollEnabled = isScrollEnabled 370 382 ) 371 383 372 384 TabBarDestinations.NOTIFICATIONS -> NotificationsView( 373 385 viewModel = timelineViewModel, 374 386 state = notificationsState, 375 387 modifier = Modifier.padding(values), 376 - isScrollEnabled = remember { mutableStateOf(isScrollEnabled) }, 388 + isScrollEnabled = isScrollEnabled, 377 389 onReplyTap = onReplyTap 378 390 ) 379 391 }
+2 -3
app/src/main/java/industries/geesawra/monarch/NotificationsView.kt
··· 11 11 import androidx.compose.material3.CircularProgressIndicator 12 12 import androidx.compose.runtime.Composable 13 13 import androidx.compose.runtime.LaunchedEffect 14 - import androidx.compose.runtime.MutableState 15 14 import androidx.compose.runtime.derivedStateOf 16 15 import androidx.compose.runtime.getValue 17 16 import androidx.compose.runtime.remember ··· 29 28 viewModel: TimelineViewModel, 30 29 state: LazyListState, 31 30 modifier: Modifier = Modifier, 32 - isScrollEnabled: MutableState<Boolean>, 31 + isScrollEnabled: Boolean, 33 32 onReplyTap: (SkeetData, Boolean) -> Unit = { _, _ -> }, 34 33 ) { 35 34 LazyColumn( 36 35 state = state, 37 36 modifier = modifier.fillMaxSize(), 38 - userScrollEnabled = isScrollEnabled.value, 37 + userScrollEnabled = isScrollEnabled, 39 38 verticalArrangement = Arrangement.spacedBy(8.dp), 40 39 ) { 41 40 viewModel.uiState.notifications.forEach { notif ->
+2 -3
app/src/main/java/industries/geesawra/monarch/ShowSkeets.kt
··· 15 15 import androidx.compose.material3.VerticalDivider 16 16 import androidx.compose.runtime.Composable 17 17 import androidx.compose.runtime.LaunchedEffect 18 - import androidx.compose.runtime.MutableState 19 18 import androidx.compose.runtime.derivedStateOf 20 19 import androidx.compose.runtime.getValue 21 20 import androidx.compose.runtime.remember ··· 30 29 fun ShowSkeets( 31 30 modifier: Modifier = Modifier, 32 31 viewModel: TimelineViewModel, 33 - isScrollEnabled: MutableState<Boolean>, 32 + isScrollEnabled: Boolean, 34 33 state: LazyListState = rememberLazyListState(), 35 34 onReplyTap: (SkeetData, Boolean) -> Unit = { _, _ -> }, 36 35 ) { 37 36 LazyColumn( 38 37 state = state, 39 - userScrollEnabled = isScrollEnabled.value, 38 + userScrollEnabled = isScrollEnabled, 40 39 modifier = modifier.fillMaxSize(), 41 40 verticalArrangement = Arrangement.spacedBy(8.dp), 42 41 ) {
+13 -15
app/src/main/java/industries/geesawra/monarch/datalayer/TimelineViewModel.kt
··· 117 117 118 118 timelineFetchJob = viewModelScope.launch { 119 119 bskyConn.fetchTimeline( 120 - { 121 - if (uiState.selectedFeed == "Following") { 122 - "" 123 - } else { 124 - uiState.selectedFeed 125 - } 126 - }(), if (fresh) { 120 + if (uiState.selectedFeed == "Following") { 121 + "" 122 + } else { 123 + uiState.selectedFeed 124 + }, if (fresh) { 127 125 null 128 126 } else { 129 127 uiState.timelineCursor 130 128 } 131 - ).onSuccess { it -> 132 - if (fresh) { 133 - uiState = uiState.copy(skeets = listOf()) 129 + ).onSuccess { response -> 130 + val newSkeets = if (fresh) { 131 + response.feed.map { SkeetData.fromFeedViewPost(it) }.distinctBy { it.cid } 132 + } else { 133 + (uiState.skeets + response.feed.map { SkeetData.fromFeedViewPost(it) }).distinctBy { it.cid } 134 134 } 135 - val newData = 136 - (uiState.skeets + it.feed.map { SkeetData.fromFeedViewPost(it) }).distinctBy { it.cid } 137 135 138 136 uiState = uiState.copy( 139 - skeets = newData, 140 - timelineCursor = it.cursor, 137 + skeets = newSkeets, 138 + timelineCursor = response.cursor, 141 139 isFetchingMoreTimeline = false 142 140 ) 143 141 then() ··· 337 335 } 338 336 } 339 337 } 340 - } 338 + }