A cheap attempt at a native Bluesky client for Android

MainView: Show user avatar in top bar

Adds the logged-in user's avatar to the right side of the top app bar on the timeline view. This required adding a `fetchSelf` method to the Bluesky data layer and the `TimelineViewModel` to retrieve the current user's profile details.

The feed avatar's shadow has also been removed.

+72 -8
+29 -2
app/src/main/java/industries/geesawra/monarch/MainView.kt
··· 67 67 import androidx.compose.ui.Alignment 68 68 import androidx.compose.ui.Modifier 69 69 import androidx.compose.ui.draw.clip 70 - import androidx.compose.ui.draw.shadow 71 70 import androidx.compose.ui.graphics.vector.ImageVector 72 71 import androidx.compose.ui.input.nestedscroll.nestedScroll 72 + import androidx.compose.ui.layout.ContentScale 73 73 import androidx.compose.ui.platform.LocalContext 74 74 import androidx.compose.ui.res.stringResource 75 75 import androidx.compose.ui.semantics.contentDescription ··· 306 306 model = timelineViewModel.uiState.feedAvatar, 307 307 modifier = Modifier 308 308 .size(40.dp) 309 - .shadow(10.dp, CircleShape) 310 309 .clip(CircleShape), 311 310 contentDescription = "Feed avatar", 312 311 ) ··· 337 336 TabBarDestinations.NOTIFICATIONS -> {} 338 337 } 339 338 }, 339 + actions = { 340 + when (currentDestination) { 341 + TabBarDestinations.TIMELINE -> { 342 + if (timelineViewModel.uiState.user == null) { 343 + return@LargeTopAppBar 344 + } 345 + 346 + val user = timelineViewModel.uiState.user!! 347 + 348 + IconButton(onClick = {}) { 349 + AsyncImage( 350 + model = ImageRequest.Builder(LocalContext.current) 351 + .data(user.avatar?.uri) 352 + .crossfade(true) 353 + .build(), 354 + contentDescription = "${user.displayName ?: user.handle.handle}'s avatar", 355 + contentScale = ContentScale.Crop, 356 + modifier = 357 + Modifier 358 + .size(55.dp) 359 + .clip(CircleShape) 360 + ) 361 + } 362 + } 363 + 364 + TabBarDestinations.NOTIFICATIONS -> {} 365 + } 366 + } 340 367 ) 341 368 }, 342 369 floatingActionButton = {
+10
app/src/main/java/industries/geesawra/monarch/datalayer/Bluesky.kt
··· 626 626 } 627 627 } 628 628 629 + suspend fun fetchSelf(): Result<ProfileViewDetailed> { 630 + return runCatching { 631 + create().onFailure { 632 + return Result.failure(LoginException(it.message)) 633 + } 634 + 635 + return fetchActor(session!!.did) 636 + } 637 + } 638 + 629 639 private data class MediaBlob( 630 640 val blob: Blob, 631 641 val width: Long,
+33 -6
app/src/main/java/industries/geesawra/monarch/datalayer/TimelineViewModel.kt
··· 39 39 40 40 41 41 data class TimelineUiState( 42 + val user: ProfileViewDetailed? = null, 42 43 val selectedFeed: String = "following", 43 44 val feedName: String = "Following", 44 45 val feedAvatar: String? = null, ··· 81 82 init { 82 83 fetchTimeline(fresh = true) 83 84 fetchNotifications(fresh = true) 85 + fetchSelf() 84 86 } 85 87 86 88 fun loadSession() { ··· 105 107 return Result.success(ret) 106 108 } 107 109 108 - suspend fun fetchActor(did: Did): Result<ProfileViewDetailed> { 110 + private suspend fun fetchActor(did: Did): Result<ProfileViewDetailed> { 109 111 val ret = bskyConn.fetchActor(did).onFailure { 110 112 uiState = when (it) { 111 113 is LoginException -> uiState.copy(loginError = it.message) ··· 114 116 }.getOrThrow() 115 117 116 118 return Result.success(ret) 119 + } 120 + 121 + fun fetchSelf() { 122 + viewModelScope.launch { 123 + val ret = bskyConn.fetchSelf().onFailure { 124 + uiState = when (it) { 125 + is LoginException -> uiState.copy(loginError = it.message) 126 + else -> uiState.copy(error = it.message) 127 + } 128 + }.onSuccess { 129 + uiState = uiState.copy(user = it) 130 + } 131 + } 117 132 } 118 133 119 134 fun fetchTimeline(fresh: Boolean = false, then: () -> Unit = {}) { ··· 558 573 } 559 574 } 560 575 561 - val currentPostSkeetData = SkeetData.fromPostView(threadUnion.value.post, threadUnion.value.post.author) 576 + val currentPostSkeetData = 577 + SkeetData.fromPostView(threadUnion.value.post, threadUnion.value.post.author) 562 578 563 579 val replies = threadUnion.value.replies.map { replyUnion -> 564 580 readThread( 565 581 threadUnion = when (replyUnion) { 566 - is ThreadViewPostReplieUnion.BlockedPost -> GetPostThreadResponseThreadUnion.BlockedPost(replyUnion.value) 567 - is ThreadViewPostReplieUnion.NotFoundPost -> GetPostThreadResponseThreadUnion.NotFoundPost(replyUnion.value) 568 - is ThreadViewPostReplieUnion.ThreadViewPost -> GetPostThreadResponseThreadUnion.ThreadViewPost(replyUnion.value) 569 - is ThreadViewPostReplieUnion.Unknown -> GetPostThreadResponseThreadUnion.Unknown(replyUnion.value) 582 + is ThreadViewPostReplieUnion.BlockedPost -> GetPostThreadResponseThreadUnion.BlockedPost( 583 + replyUnion.value 584 + ) 585 + 586 + is ThreadViewPostReplieUnion.NotFoundPost -> GetPostThreadResponseThreadUnion.NotFoundPost( 587 + replyUnion.value 588 + ) 589 + 590 + is ThreadViewPostReplieUnion.ThreadViewPost -> GetPostThreadResponseThreadUnion.ThreadViewPost( 591 + replyUnion.value 592 + ) 593 + 594 + is ThreadViewPostReplieUnion.Unknown -> GetPostThreadResponseThreadUnion.Unknown( 595 + replyUnion.value 596 + ) 570 597 }, 571 598 level = level + 1 572 599 )