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

Configure Feed

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

MainView: Switch to standard TopAppBar and add image placeholders

Update `MainView` to use `TopAppBar` with `enterAlwaysScrollBehavior` instead of the flexible variant. Additionally, provide a consistent `ColorPainter` placeholder using the `surfaceVariant` color for `AsyncImage` components across `MainView`, `SkeetView`, `ComposeView`, and other gallery/list items to improve the loading experience.

geesawra 7427731b 738e2e06

+26 -6
+3
app/src/main/java/industries/geesawra/monarch/ComposeView.kt
··· 98 98 import androidx.compose.foundation.rememberScrollState 99 99 import industries.geesawra.monarch.datalayer.LinkPreviewData 100 100 import industries.geesawra.monarch.datalayer.LinkPreviewFetcher 101 + import androidx.compose.ui.graphics.painter.ColorPainter 101 102 import coil3.compose.AsyncImage 102 103 import coil3.request.ImageRequest 103 104 import androidx.compose.ui.platform.LocalContext ··· 488 489 model = ImageRequest.Builder(LocalContext.current) 489 490 .data(profile.avatar?.uri) 490 491 .build(), 492 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 491 493 contentDescription = "${profile.displayName ?: profile.handle.handle}'s avatar", 492 494 contentScale = ContentScale.Crop, 493 495 modifier = Modifier ··· 545 547 model = ImageRequest.Builder(LocalContext.current) 546 548 .data(imgUrl) 547 549 .build(), 550 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 548 551 contentScale = ContentScale.Crop, 549 552 contentDescription = "Link preview thumbnail", 550 553 modifier = Modifier
+3
app/src/main/java/industries/geesawra/monarch/LikeRepostRowView.kt
··· 45 45 import androidx.compose.ui.unit.IntSize 46 46 import androidx.compose.ui.unit.dp 47 47 import app.bsky.actor.ProfileView 48 + import androidx.compose.ui.graphics.painter.ColorPainter 48 49 import coil3.compose.AsyncImage 49 50 import coil3.request.ImageRequest 50 51 import coil3.request.crossfade ··· 165 166 .data(it.author.avatar?.uri) 166 167 .crossfade(true) 167 168 .build(), 169 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 168 170 contentDescription = "Avatar", 169 171 modifier = Modifier 170 172 .size( ··· 209 211 .data(it.author.avatar?.uri) 210 212 .crossfade(true) 211 213 .build(), 214 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 212 215 contentDescription = "Avatar", 213 216 modifier = Modifier 214 217 .size(
+10 -6
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 47 47 import androidx.compose.material3.Snackbar 48 48 import androidx.compose.material3.SnackbarHost 49 49 import androidx.compose.material3.Text 50 + import androidx.compose.material3.TopAppBar 50 51 import androidx.compose.material3.TopAppBarColors 51 52 import androidx.compose.material3.TopAppBarDefaults 52 53 import androidx.compose.material3.WideNavigationRailItem ··· 79 80 import androidx.compose.ui.semantics.semantics 80 81 import androidx.compose.ui.text.font.FontWeight 81 82 import androidx.compose.ui.unit.dp 83 + import androidx.compose.ui.graphics.painter.ColorPainter 82 84 import coil3.compose.AsyncImage 83 85 import coil3.request.ImageRequest 84 86 import coil3.request.crossfade ··· 205 207 onSeeMoreTap: (SkeetData) -> Unit, 206 208 ) { 207 209 var currentDestination by rememberSaveable { mutableStateOf(TabBarDestinations.TIMELINE) } 208 - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( 210 + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( 209 211 rememberTopAppBarState() 210 212 ) 211 213 val timelineState = rememberLazyListState() ··· 307 309 .fillMaxSize() 308 310 .nestedScroll(scrollBehavior.nestedScrollConnection), 309 311 topBar = { 310 - MediumFlexibleTopAppBar( 311 - colors = TopAppBarColors( 312 + TopAppBar( 313 + colors = TopAppBarDefaults.topAppBarColors( 312 314 containerColor = MaterialTheme.colorScheme.background, 313 315 scrolledContainerColor = MaterialTheme.colorScheme.background, 314 - navigationIconContentColor = MaterialTheme.colorScheme.onBackground, // Ensuring correct contrast 316 + navigationIconContentColor = MaterialTheme.colorScheme.onBackground, 315 317 titleContentColor = MaterialTheme.colorScheme.onBackground, 316 318 actionIconContentColor = MaterialTheme.colorScheme.onBackground, 317 - subtitleContentColor = MaterialTheme.colorScheme.onBackground 318 319 ), 319 320 title = { 320 321 when (currentDestination) { ··· 325 326 if (timelineViewModel.uiState.feedAvatar != null) { 326 327 AsyncImage( 327 328 model = timelineViewModel.uiState.feedAvatar, 329 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 328 330 modifier = Modifier 329 331 .size(40.dp) 330 332 .clip(CircleShape), ··· 361 363 when (currentDestination) { 362 364 TabBarDestinations.TIMELINE -> { 363 365 if (timelineViewModel.uiState.user == null) { 364 - return@MediumFlexibleTopAppBar 366 + return@TopAppBar 365 367 } 366 368 367 369 val user = timelineViewModel.uiState.user!! ··· 372 374 .data(user.avatar?.uri) 373 375 .crossfade(true) 374 376 .build(), 377 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 375 378 contentDescription = "${user.displayName ?: user.handle.handle}'s avatar", 376 379 contentScale = ContentScale.Crop, 377 380 modifier = ··· 592 595 .data(feed.avatar?.uri) 593 596 .crossfade(true) 594 597 .build(), 598 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 595 599 modifier = Modifier 596 600 .size(20.dp) 597 601 .clip(CircleShape),
+3
app/src/main/java/industries/geesawra/monarch/PostImageGallery.kt
··· 25 25 import androidx.compose.ui.layout.ContentScale 26 26 import androidx.compose.ui.platform.LocalContext 27 27 import androidx.compose.ui.unit.dp 28 + import androidx.compose.material3.MaterialTheme 29 + import androidx.compose.ui.graphics.painter.ColorPainter 28 30 import coil3.compose.AsyncImage 29 31 import coil3.request.ImageRequest 30 32 import coil3.request.crossfade ··· 212 214 .data(image.url) 213 215 .crossfade(true) 214 216 .build(), 217 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 215 218 contentDescription = image.alt, 216 219 contentScale = ContentScale.Crop, 217 220 modifier = if (aspectRatio != null) {
+7
app/src/main/java/industries/geesawra/monarch/SkeetView.kt
··· 73 73 import app.bsky.feed.FeedViewPostReasonUnion 74 74 import app.bsky.feed.PostViewEmbedUnion 75 75 import app.bsky.feed.ReplyRefParentUnion 76 + import androidx.compose.ui.graphics.painter.ColorPainter 76 77 import coil3.compose.AsyncImage 77 78 import coil3.request.ImageRequest 78 79 import coil3.request.crossfade ··· 144 145 .data(skeet.authorAvatarURL) 145 146 .crossfade(true) 146 147 .build(), 148 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 147 149 contentDescription = "Avatar", 148 150 modifier = Modifier 149 151 .size(minSize) ··· 186 188 .data(skeet.authorAvatarURL) 187 189 .crossfade(true) 188 190 .build(), 191 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 189 192 contentDescription = "Avatar", 190 193 modifier = Modifier 191 194 .size(minSize) ··· 426 429 .data(it.uri) 427 430 .crossfade(true) 428 431 .build(), 432 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 429 433 contentScale = ContentScale.Crop, 430 434 alignment = Alignment.Center, 431 435 contentDescription = "External link thumbnail", ··· 599 603 .data(avatarUri.uri) 600 604 .crossfade(true) 601 605 .build(), 606 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 602 607 contentDescription = null, 603 608 modifier = Modifier 604 609 .size(18.dp) ··· 758 763 .data(avatarUrl) 759 764 .crossfade(true) 760 765 .build(), 766 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 761 767 contentDescription = definition.plaintext, 762 768 modifier = Modifier 763 769 .size(14.dp) ··· 823 829 .data(avatarUrl) 824 830 .crossfade(true) 825 831 .build(), 832 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 826 833 contentDescription = definition.plaintext, 827 834 modifier = Modifier 828 835 .size(14.dp)