A cheap attempt at a native Bluesky client for Android

SkeetView: show raw labels

+93 -20
+27 -17
app/src/main/java/industries/geesawra/monarch/SkeetView.kt
··· 9 9 import androidx.compose.foundation.clickable 10 10 import androidx.compose.foundation.layout.Arrangement 11 11 import androidx.compose.foundation.layout.Column 12 + import androidx.compose.foundation.layout.ExperimentalLayoutApi 13 + import androidx.compose.foundation.layout.FlowRow 12 14 import androidx.compose.foundation.layout.Row 13 15 import androidx.compose.foundation.layout.fillMaxSize 14 16 import androidx.compose.foundation.layout.fillMaxWidth ··· 19 21 import androidx.compose.foundation.layout.sizeIn 20 22 import androidx.compose.foundation.shape.CircleShape 21 23 import androidx.compose.material3.Card 22 - import androidx.compose.material3.FilterChip 23 24 import androidx.compose.material3.HorizontalDivider 24 25 import androidx.compose.material3.MaterialTheme 25 26 import androidx.compose.material3.OutlinedCard ··· 32 33 import androidx.compose.ui.graphics.Color 33 34 import androidx.compose.ui.layout.ContentScale 34 35 import androidx.compose.ui.platform.LocalContext 36 + import androidx.compose.ui.text.TextStyle 35 37 import androidx.compose.ui.text.font.FontWeight 36 38 import androidx.compose.ui.text.style.TextAlign 37 39 import androidx.compose.ui.unit.dp 40 + import androidx.compose.ui.unit.sp 38 41 import androidx.core.net.toUri 39 42 import androidx.media3.common.MimeTypes 40 43 import app.bsky.embed.ExternalViewExternal ··· 393 396 } 394 397 } 395 398 399 + @OptIn(ExperimentalLayoutApi::class) 396 400 @Composable 397 401 private fun SkeetHeader(skeet: SkeetData, modifier: Modifier = Modifier) { 398 402 val authorName = skeet.authorName ?: (skeet.authorHandle?.handle ?: "") ··· 430 434 style = MaterialTheme.typography.bodySmall, 431 435 ) 432 436 433 - skeet.authorLabels.forEach { 434 - it.neg?.let { it -> 435 - if (!it) { 437 + FlowRow( 438 + horizontalArrangement = Arrangement.spacedBy(4.dp), 439 + modifier = Modifier.padding(top = 4.dp) 440 + ) { 441 + skeet.authorLabels.forEach { 442 + it.neg?.let { it -> 443 + if (!it) { 444 + return@forEach 445 + } 446 + } 447 + if (it.`val`.startsWith("!")) { 436 448 return@forEach 437 449 } 438 - } 439 - if (it.`val`.startsWith("!")) { 440 - return@forEach 441 - } 442 450 443 - FilterChip( 444 - leadingIcon = { 445 - }, 446 - enabled = true, 447 - onClick = {}, 448 - selected = true, 449 - label = { 450 - Text(text = it.`val`) 451 + Card( 452 + modifier = Modifier.padding(end = 4.dp, bottom = 4.dp), 453 + shape = CircleShape 454 + ) { 455 + Text( 456 + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), 457 + text = it.`val`, 458 + style = TextStyle(fontSize = 12.sp), 459 + ) 451 460 } 452 - ) 461 + } 453 462 } 463 + 454 464 455 465 skeet.reply?.let { 456 466 it
+65 -2
app/src/main/java/industries/geesawra/monarch/datalayer/Bluesky.kt
··· 28 28 import app.bsky.feed.PostEmbedUnion 29 29 import app.bsky.feed.PostReplyRef 30 30 import app.bsky.feed.Repost 31 + import app.bsky.labeler.GetServicesQueryParams 32 + import app.bsky.labeler.GetServicesResponse 33 + import app.bsky.labeler.GetServicesResponseViewUnion 31 34 import app.bsky.notification.ListNotificationsQueryParams 32 35 import app.bsky.notification.ListNotificationsResponse 33 36 import app.bsky.video.GetJobStatusQueryParams ··· 292 295 val message: String?, 293 296 ) 294 297 295 - private suspend fun refreshIfNeeded(pdsURL: String, token: SessionData): Result<Unit> { 298 + private suspend fun refreshIfNeeded( 299 + pdsURL: String, 300 + token: SessionData, 301 + labelers: List<String>? = listOf() 302 + ): Result<Unit> { 296 303 return runCatching { 297 304 val httpClient = HttpClient(OkHttp) { 298 305 defaultRequest { 299 306 url(pdsURL) 307 + labelers?.let { 308 + headers["atproto-accept-labelers"] = labelers.joinToString() 309 + } 300 310 } 301 311 install(HttpTimeout) { 302 312 requestTimeoutMillis = 15000 ··· 314 324 } 315 325 316 326 when (gs.status) { 317 - 318 327 HttpStatusCode.OK -> run { 319 328 this.session = token 320 329 val tokens = ··· 411 420 412 421 this.pdsURL = pdsURL 413 422 423 + val labelers = this.subscribedLabelers().getOrThrow().keys.mapNotNull { it?.did } 424 + 425 + refreshIfNeeded(pdsURL, sessionData, labelers).onFailure { 426 + createMutex.unlock() 427 + return Result.failure(it) 428 + } 429 + 414 430 createMutex.unlock() 415 431 } 416 432 } ··· 734 750 ).requireResponse() 735 751 736 752 return Result.success(resp.feeds) 753 + } 754 + } 755 + 756 + suspend fun subscribedLabelers(): Result<Map<Did?, GetServicesResponseViewUnion.LabelerViewDetailed?>> { 757 + return runCatching { 758 + val prefs = client!!.getPreferences().requireResponse() 759 + val labelers = (prefs.preferences.first { 760 + when (it) { 761 + is PreferencesUnion.LabelersPref -> true 762 + else -> false 763 + } 764 + } as PreferencesUnion.LabelersPref).value.labelers.map { it.did }.toMutableList() 765 + 766 + val otherOnes = (prefs.preferences.filter { 767 + when (it) { 768 + is PreferencesUnion.ContentLabelPref -> true 769 + else -> false 770 + } 771 + }.map { it as PreferencesUnion.ContentLabelPref }).forEach { 772 + it.value.labelerDid?.let { did -> 773 + labelers += did 774 + } 775 + } 776 + 777 + val res = client!!.getServices( 778 + GetServicesQueryParams( 779 + detailed = true, 780 + dids = labelers 781 + ) 782 + ) 783 + 784 + val asd = when (res) { 785 + is AtpResponse.Failure<*> -> return Result.failure(Exception("Failed to fetch subscribed labelers: ${res.error}")) 786 + is AtpResponse.Success<GetServicesResponse> -> { 787 + res 788 + } 789 + } 790 + 791 + val kek = asd.response.views.associate { 792 + when (it) { 793 + is GetServicesResponseViewUnion.LabelerView -> it.value.uri.did() to null 794 + is GetServicesResponseViewUnion.LabelerViewDetailed -> it.value.uri.did() to it 795 + is GetServicesResponseViewUnion.Unknown -> null to null 796 + } 797 + }.filter { it.value != null && it.key != null } 798 + 799 + return Result.success(kek) 737 800 } 738 801 } 739 802
+1 -1
app/src/main/java/industries/geesawra/monarch/datalayer/TimelineViewModel.kt
··· 244 244 } 245 245 } 246 246 } 247 - 247 + 248 248 fun selectFeed(uri: String, displayName: String, avatar: String?, then: () -> Unit = {}) { 249 249 uiState = uiState.copy( 250 250 selectedFeed = uri,