A cheap attempt at a native Bluesky client for Android

MainView: Use ModalWideNavigationRail for feeds

Replace the `ModalNavigationDrawer` with the new `ModalWideNavigationRail` from Material 3 to display the list of user feeds.

This change updates the slide-out feeds drawer to a side rail, which is more suitable for larger screens. The `FeedsDrawer` composable has been updated to use `WideNavigationRailItem` instead of `NavigationDrawerItem`.

+85 -80
+85 -80
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 14 14 import androidx.compose.foundation.layout.Spacer 15 15 import androidx.compose.foundation.layout.WindowInsets 16 16 import androidx.compose.foundation.layout.fillMaxSize 17 + import androidx.compose.foundation.layout.fillMaxWidth 17 18 import androidx.compose.foundation.layout.padding 18 19 import androidx.compose.foundation.layout.size 19 20 import androidx.compose.foundation.layout.statusBars ··· 30 31 import androidx.compose.material3.Badge 31 32 import androidx.compose.material3.BadgedBox 32 33 import androidx.compose.material3.BottomSheetScaffold 33 - import androidx.compose.material3.DrawerValue 34 34 import androidx.compose.material3.ExperimentalMaterial3Api 35 35 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 36 36 import androidx.compose.material3.FloatingActionButton ··· 39 39 import androidx.compose.material3.IconButton 40 40 import androidx.compose.material3.LargeTopAppBar 41 41 import androidx.compose.material3.MaterialTheme 42 - import androidx.compose.material3.ModalDrawerSheet 43 - import androidx.compose.material3.ModalNavigationDrawer 42 + import androidx.compose.material3.ModalWideNavigationRail 44 43 import androidx.compose.material3.NavigationBar 45 44 import androidx.compose.material3.NavigationBarItem 46 - import androidx.compose.material3.NavigationDrawerItem 47 - import androidx.compose.material3.NavigationDrawerItemDefaults 48 45 import androidx.compose.material3.Scaffold 49 46 import androidx.compose.material3.Snackbar 50 47 import androidx.compose.material3.SnackbarHost 51 48 import androidx.compose.material3.Text 52 49 import androidx.compose.material3.TopAppBarColors 53 50 import androidx.compose.material3.TopAppBarDefaults 51 + import androidx.compose.material3.WideNavigationRailItem 52 + import androidx.compose.material3.WideNavigationRailValue 54 53 import androidx.compose.material3.pulltorefresh.PullToRefreshBox 55 54 import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.LoadingIndicator 56 55 import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState 57 56 import androidx.compose.material3.rememberBottomSheetScaffoldState 58 - import androidx.compose.material3.rememberDrawerState 59 57 import androidx.compose.material3.rememberModalBottomSheetState 60 58 import androidx.compose.material3.rememberTopAppBarState 59 + import androidx.compose.material3.rememberWideNavigationRailState 61 60 import androidx.compose.runtime.Composable 62 61 import androidx.compose.runtime.LaunchedEffect 63 62 import androidx.compose.runtime.MutableIntState ··· 210 209 ) 211 210 val timelineState = rememberLazyListState() 212 211 val notificationsState = rememberLazyListState() 213 - val drawerState = rememberDrawerState( 214 - initialValue = DrawerValue.Closed 212 + val drawerState = rememberWideNavigationRailState( 213 + initialValue = WideNavigationRailValue.Collapsed 215 214 ) 216 215 val isRefreshing = 217 216 timelineViewModel.uiState.isFetchingMoreTimeline || timelineViewModel.uiState.isFetchingMoreNotifications ··· 257 256 } 258 257 }, 259 258 ) { 260 - ModalNavigationDrawer( 261 - drawerState = drawerState, 262 - modifier = modifier, 263 - drawerContent = { 264 - FeedsDrawer( 265 - { uri: String, displayName: String, avatar: String? -> 266 - timelineViewModel.selectFeed( 267 - uri, 268 - displayName, 269 - avatar 270 - ) { 259 + Row(modifier = Modifier.fillMaxSize()) { 260 + ModalWideNavigationRail( 261 + header = { 262 + Text( 263 + text = "Feeds", 264 + modifier = Modifier.padding(start = 16.dp), 265 + fontWeight = FontWeight.Bold, 266 + style = MaterialTheme.typography.titleLarge 267 + ) 268 + }, 269 + hideOnCollapse = true, 270 + state = drawerState, 271 + modifier = modifier, 272 + content = { 273 + FeedsDrawer( 274 + state = drawerState.targetValue, 275 + { uri: String, displayName: String, avatar: String? -> 271 276 coroutineScope.launch { 272 - launch { 273 - timelineState.scrollToItem(0) 274 - } 275 - launch { 276 - drawerState.close() 277 + drawerState.collapse() 278 + } 279 + timelineViewModel.selectFeed( 280 + uri, 281 + displayName, 282 + avatar 283 + ) { 284 + coroutineScope.launch { 285 + launch { 286 + timelineState.scrollToItem(0) 287 + } 277 288 } 278 289 } 279 - } 280 - }, 281 - timelineViewModel 282 - ) 283 - } 284 - ) { 290 + }, 291 + timelineViewModel 292 + ) 293 + } 294 + ) 295 + } 296 + 297 + Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { 285 298 LaunchedEffect(timelineViewModel.uiState.unreadNotificationsAmt) { 286 299 TabBarDestinations.NOTIFICATIONS.badgeValue?.intValue = 287 300 timelineViewModel.uiState.unreadNotificationsAmt ··· 334 347 when (currentDestination) { 335 348 TabBarDestinations.TIMELINE -> IconButton(onClick = { 336 349 coroutineScope.launch { 337 - drawerState.open() 350 + drawerState.expand() 338 351 } 339 352 }) { 340 353 Icon(Icons.Default.Tag, "Feeds") ··· 403 416 initialValue = scrollBehavior.state.heightOffset, 404 417 targetValue = 0f 405 418 ) { value, /* velocity */ _ -> 406 - scrollBehavior.state.heightOffset = value 419 + scrollBehavior.state.heightOffset = 420 + value 407 421 } 408 422 } 409 423 } ··· 534 548 } 535 549 } 536 550 551 + 537 552 @Composable 538 553 fun FeedsDrawer( 554 + state: WideNavigationRailValue, 539 555 selectFeed: (uri: String, displayName: String, avatar: String?) -> Unit, 540 556 timelineViewModel: TimelineViewModel, 541 557 ) { 542 - ModalDrawerSheet { 543 - Text( 544 - "Feeds", 545 - modifier = Modifier.padding(16.dp), 546 - style = MaterialTheme.typography.titleLarge, 547 - fontWeight = FontWeight.Bold 548 - ) 549 - NavigationDrawerItem( 558 + WideNavigationRailItem( 559 + label = { 560 + Text(text = "Following") 561 + }, 562 + selected = timelineViewModel.uiState.selectedFeed.lowercase() == "following", 563 + onClick = { 564 + selectFeed("following", "Following", null) 565 + }, 566 + icon = { 567 + Spacer(modifier = Modifier.size(20.dp)) 568 + }, 569 + railExpanded = state == WideNavigationRailValue.Expanded, 570 + ) 571 + 572 + timelineViewModel.uiState.feeds.forEach { feed -> 573 + WideNavigationRailItem( 550 574 label = { 551 575 Row( 552 576 horizontalArrangement = Arrangement.spacedBy(8.dp), 553 577 verticalAlignment = Alignment.CenterVertically 554 578 ) { 555 - Spacer(modifier = Modifier.size(20.dp)) 556 - Text(text = "Following") 579 + Text(text = feed.displayName) 557 580 } 558 581 }, 559 - selected = timelineViewModel.uiState.selectedFeed.lowercase() == "following", 560 - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), 582 + selected = timelineViewModel.uiState.selectedFeed == feed.uri.atUri, 561 583 onClick = { 562 - selectFeed("following", "Following", null) 563 - } 564 - ) 565 - 566 - timelineViewModel.uiState.feeds.forEach { feed -> 567 - NavigationDrawerItem( 568 - label = { 569 - Row( 570 - horizontalArrangement = Arrangement.spacedBy(8.dp), 571 - verticalAlignment = Alignment.CenterVertically 572 - ) { 573 - if (feed.avatar != null) { 574 - AsyncImage( 575 - model = ImageRequest.Builder(LocalContext.current) 576 - .data(feed.avatar?.uri) 577 - .crossfade(true) 578 - .build(), 579 - modifier = Modifier 580 - .size(20.dp) 581 - .clip(CircleShape), 582 - contentDescription = "Feed avatar", 583 - ) 584 - } else { 585 - Spacer(modifier = Modifier.size(20.dp)) 586 - } 587 - 588 - Text(text = feed.displayName) 589 - } 590 - }, 591 - selected = timelineViewModel.uiState.selectedFeed == feed.uri.atUri, 592 - modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding), 593 - onClick = { 594 - selectFeed(feed.uri.atUri, feed.displayName, feed.avatar?.uri) 584 + selectFeed(feed.uri.atUri, feed.displayName, feed.avatar?.uri) 585 + }, 586 + icon = { 587 + if (feed.avatar != null) { 588 + AsyncImage( 589 + model = ImageRequest.Builder(LocalContext.current) 590 + .data(feed.avatar?.uri) 591 + .crossfade(true) 592 + .build(), 593 + modifier = Modifier 594 + .size(20.dp) 595 + .clip(CircleShape), 596 + contentDescription = "Feed avatar", 597 + ) 598 + } else { 599 + Spacer(modifier = Modifier.size(20.dp)) 595 600 } 596 - ) 597 - } 598 - 601 + }, 602 + railExpanded = state == WideNavigationRailValue.Expanded, 603 + ) 599 604 } 600 605 }