A cheap attempt at a native Bluesky client for Android

MainView: Refresh timeline and notifications simultaneously

When pulling to refresh, both the timeline and notifications feeds are now fetched at the same time. After the refresh completes, the view scrolls to the top of both lists.

This also changes the top app bar's scroll behavior to `exitUntilCollapsed`.

+50 -32
+31 -24
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 197 197 onSeeMoreTap: (SkeetData) -> Unit, 198 198 ) { 199 199 var currentDestination by rememberSaveable { mutableStateOf(TabBarDestinations.TIMELINE) } 200 - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( 200 + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 201 201 rememberTopAppBarState() 202 202 ) 203 203 val timelineState = rememberLazyListState() ··· 207 207 ) 208 208 val isRefreshing = 209 209 timelineViewModel.uiState.isFetchingMoreTimeline || timelineViewModel.uiState.isFetchingMoreNotifications 210 - val isScrollEnabled = !isRefreshing 210 + val isScrollEnabled = true 211 211 val ctx = LocalContext.current 212 212 213 - LaunchedEffect(Unit) { 214 - timelineViewModel.feeds() 215 - } 216 - 217 213 218 214 LaunchedEffect(timelineViewModel.uiState.loginError) { 219 215 timelineViewModel.uiState.loginError?.let { ··· 232 228 PullToRefreshBox( 233 229 isRefreshing = isRefreshing, 234 230 onRefresh = { 235 - when (currentDestination) { 236 - TabBarDestinations.TIMELINE -> { 237 - timelineViewModel.fetchTimeline(fresh = true) { 238 - coroutineScope.launch { 239 - timelineState.scrollToItem(0) 240 - } 231 + timelineViewModel.fetchAllNewData() { 232 + coroutineScope.launch { 233 + launch { 234 + timelineState.scrollToItem(0) 241 235 } 242 - } 243 - 244 - TabBarDestinations.NOTIFICATIONS -> { 245 - timelineViewModel.fetchNotifications(fresh = true) { 246 - coroutineScope.launch { 247 - notificationsState.scrollToItem(0) 248 - } 236 + launch { 237 + notificationsState.scrollToItem(0) 249 238 } 250 239 } 251 240 } 241 + // when (currentDestination) { 242 + // TabBarDestinations.TIMELINE -> { 243 + // timelineViewModel.fetchTimeline(fresh = true) { 244 + // coroutineScope.launch { 245 + // timelineState.scrollToItem(0) 246 + // } 247 + // } 248 + // } 249 + // 250 + // TabBarDestinations.NOTIFICATIONS -> { 251 + // timelineViewModel.fetchNotifications(fresh = true) { 252 + // coroutineScope.launch { 253 + // notificationsState.scrollToItem(0) 254 + // } 255 + // } 256 + // } 257 + // } 252 258 }, 253 259 ) { 254 260 ModalNavigationDrawer( ··· 263 269 avatar 264 270 ) { 265 271 coroutineScope.launch { 266 - timelineState.scrollToItem(0) 272 + launch { 273 + timelineState.scrollToItem(0) 274 + } 275 + launch { 276 + drawerState.close() 277 + } 267 278 } 268 - } 269 - 270 - coroutineScope.launch { 271 - drawerState.close() 272 279 } 273 280 }, 274 281 timelineViewModel
+1 -1
app/src/main/java/industries/geesawra/monarch/datalayer/Bluesky.kt
··· 643 643 ) 644 644 645 645 private suspend fun uploadImages(images: List<Uri>): Result<List<MediaBlob>> { 646 - val maxImageSize = 1000000 646 + val maxImageSize = 950000 // ~950kb 647 647 648 648 return runCatching { 649 649 create().onFailure {
+18 -7
app/src/main/java/industries/geesawra/monarch/datalayer/TimelineViewModel.kt
··· 26 26 import dagger.assisted.AssistedInject 27 27 import dagger.hilt.android.lifecycle.HiltViewModel 28 28 import kotlinx.coroutines.Job 29 + import kotlinx.coroutines.joinAll 29 30 import kotlinx.coroutines.launch 30 31 import kotlinx.datetime.toStdlibInstant 31 32 import sh.christian.ozone.api.AtUri ··· 80 81 private var notificationsFetchJob: Job? = null 81 82 82 83 init { 83 - fetchTimeline(fresh = true) 84 - fetchNotifications(fresh = true) 85 - fetchSelf() 84 + fetchAllNewData() 86 85 } 87 86 88 87 fun loadSession() { ··· 93 92 } 94 93 95 94 uiState = uiState.copy(authenticated = true, sessionChecked = true) 95 + } 96 + } 97 + 98 + fun fetchAllNewData(then: () -> Unit = {}) { 99 + fetchTimeline(fresh = true) 100 + fetchNotifications(fresh = true) 101 + val fsJob = fetchSelf() 102 + val fJob = feeds() 103 + 104 + viewModelScope.launch { 105 + joinAll(timelineFetchJob!!, notificationsFetchJob!!, fsJob, fJob) 106 + then() 96 107 } 97 108 } 98 109 ··· 118 129 return Result.success(ret) 119 130 } 120 131 121 - fun fetchSelf() { 122 - viewModelScope.launch { 132 + fun fetchSelf(): Job { 133 + return viewModelScope.launch { 123 134 val ret = bskyConn.fetchSelf().onFailure { 124 135 uiState = when (it) { 125 136 is LoginException -> uiState.copy(loginError = it.message) ··· 462 473 ) // TODO: maybe refactor this to use uistate.Error? 463 474 } 464 475 465 - fun feeds() { 466 - viewModelScope.launch { 476 + fun feeds(): Job { 477 + return viewModelScope.launch { 467 478 bskyConn.feeds().onFailure { 468 479 uiState = when (it) { 469 480 is LoginException -> uiState.copy(loginError = it.message)