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.

ProfileView: Collapse avatar into top bar on scroll and fix editor layout

Show the user's avatar next to display name in the top bar when the
profile header is scrolled out of view. Remove redundant center spinner
in favor of the existing PullToRefreshBox indicator. Overlap avatar on
banner in the edit profile sheet to match the profile view layout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

geesawra b709d693 60ac0ee8

+62 -40
+62 -40
app/src/main/java/industries/geesawra/monarch/ProfileView.kt
··· 105 105 val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) 106 106 val listState = rememberLazyListState() 107 107 val profile = timelineViewModel.uiState.profileUser 108 - val isLoading = timelineViewModel.uiState.isFetchingProfile 108 + val isLoading = timelineViewModel.uiState.isFetchingProfile || timelineViewModel.uiState.isFetchingProfileFeed 109 + 110 + // Show avatar in top bar once the header item is scrolled past 111 + val showAvatarInBar by remember { 112 + derivedStateOf { 113 + listState.firstVisibleItemIndex > 0 114 + } 115 + } 109 116 110 117 PullToRefreshBox( 111 118 modifier = modifier.windowInsetsPadding(WindowInsets.statusBars), ··· 137 144 } 138 145 }, 139 146 title = { 140 - Text( 141 - text = profile?.displayName ?: profile?.handle?.handle ?: "", 142 - maxLines = 1, 143 - overflow = TextOverflow.Ellipsis, 144 - ) 147 + Row( 148 + verticalAlignment = Alignment.CenterVertically, 149 + horizontalArrangement = Arrangement.spacedBy(8.dp), 150 + ) { 151 + if (showAvatarInBar && profile?.avatar != null) { 152 + AsyncImage( 153 + model = ImageRequest.Builder(LocalContext.current) 154 + .data(profile.avatar?.uri) 155 + .crossfade(true) 156 + .build(), 157 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 158 + contentDescription = null, 159 + contentScale = ContentScale.Crop, 160 + modifier = Modifier 161 + .size(32.dp) 162 + .clip(CircleShape) 163 + ) 164 + } 165 + Text( 166 + text = profile?.displayName ?: profile?.handle?.handle ?: "", 167 + maxLines = 1, 168 + overflow = TextOverflow.Ellipsis, 169 + ) 170 + } 145 171 }, 146 172 scrollBehavior = scrollBehavior, 147 173 actions = { ··· 152 178 ) 153 179 }, 154 180 ) { padding -> 155 - if (profile == null && isLoading) { 156 - Box( 157 - modifier = Modifier 158 - .fillMaxSize() 159 - .padding(padding), 160 - contentAlignment = Alignment.Center 161 - ) { 162 - CircularProgressIndicator() 163 - } 164 - return@Scaffold 165 - } 166 - 167 181 if (profile == null) { 168 182 Box( 169 183 modifier = Modifier ··· 562 576 fontWeight = FontWeight.Bold, 563 577 ) 564 578 565 - // Banner picker 579 + // Banner + avatar overlapping 566 580 Box( 567 581 modifier = Modifier 568 582 .fillMaxWidth() 569 - .height(120.dp) 570 - .clip(MaterialTheme.shapes.medium) 571 - .clickable { bannerPicker.launch("image/*") }, 572 - contentAlignment = Alignment.Center, 583 + .padding(bottom = 32.dp), 573 584 ) { 574 - AsyncImage( 575 - model = bannerUri ?: profile.banner?.uri, 576 - placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 577 - contentDescription = "Banner", 578 - contentScale = ContentScale.Crop, 585 + // Banner 586 + Box( 579 587 modifier = Modifier 580 - .fillMaxSize() 581 - .clip(MaterialTheme.shapes.medium), 582 - ) 583 - FilledTonalIconButton(onClick = { bannerPicker.launch("image/*") }) { 584 - Icon(Icons.Default.CameraAlt, "Change banner") 588 + .fillMaxWidth() 589 + .height(120.dp) 590 + .clip(MaterialTheme.shapes.medium) 591 + .clickable { bannerPicker.launch("image/*") }, 592 + contentAlignment = Alignment.Center, 593 + ) { 594 + AsyncImage( 595 + model = bannerUri ?: profile.banner?.uri, 596 + placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant), 597 + contentDescription = "Banner", 598 + contentScale = ContentScale.Crop, 599 + modifier = Modifier 600 + .fillMaxSize() 601 + .clip(MaterialTheme.shapes.medium), 602 + ) 603 + FilledTonalIconButton(onClick = { bannerPicker.launch("image/*") }) { 604 + Icon(Icons.Default.CameraAlt, "Change banner") 605 + } 585 606 } 586 - } 587 607 588 - // Avatar picker 589 - Row( 590 - modifier = Modifier.fillMaxWidth(), 591 - horizontalArrangement = Arrangement.Center, 592 - ) { 593 - Box(contentAlignment = Alignment.BottomEnd) { 608 + // Avatar overlapping the banner 609 + Box( 610 + modifier = Modifier 611 + .align(Alignment.BottomStart) 612 + .padding(start = 8.dp) 613 + .offset(y = 32.dp), 614 + contentAlignment = Alignment.BottomEnd, 615 + ) { 594 616 AsyncImage( 595 617 model = avatarUri ?: profile.avatar?.uri, 596 618 placeholder = ColorPainter(MaterialTheme.colorScheme.surfaceVariant),