A cheap attempt at a native Bluesky client for Android

ThreadView: better handling of nested threads

+58 -79
+8 -17
app/src/main/java/industries/geesawra/monarch/ThreadView.kt
··· 1 1 package industries.geesawra.monarch 2 2 3 - import android.util.Log 4 3 import androidx.compose.foundation.layout.Column 5 4 import androidx.compose.foundation.layout.fillMaxSize 6 5 import androidx.compose.foundation.layout.padding 7 6 import androidx.compose.material3.ExperimentalMaterial3Api 8 - import androidx.compose.material3.HorizontalDivider 9 7 import androidx.compose.material3.MaterialTheme 10 8 import androidx.compose.material3.Scaffold 11 9 import androidx.compose.material3.Text ··· 20 18 import androidx.compose.runtime.remember 21 19 import androidx.compose.ui.Modifier 22 20 import androidx.compose.ui.input.nestedscroll.nestedScroll 23 - import androidx.compose.ui.util.fastForEach 24 21 import industries.geesawra.monarch.datalayer.TimelineViewModel 25 22 import kotlinx.coroutines.CoroutineScope 26 23 ··· 66 63 ) { padding -> 67 64 LaunchedEffect(Unit) { 68 65 timelineViewModel.getThread { 69 - Log.d("ThreadView", "Thread retrieved") 70 66 isRefreshing.value = false 71 67 } 72 68 } ··· 74 70 Column( 75 71 modifier = Modifier.padding(padding) 76 72 ) { 77 - timelineViewModel.uiState.currentlyShownThread.fastForEach { threadView -> 78 - ShowSkeets( 79 - viewModel = timelineViewModel, 80 - isScrollEnabled = true, 81 - data = threadView, 82 - shouldFetchMoreData = false, 83 - isShowingThread = true, 84 - ) 85 - 86 - HorizontalDivider() 87 - } 73 + ShowSkeets( 74 + viewModel = timelineViewModel, 75 + isScrollEnabled = true, 76 + data = timelineViewModel.uiState.currentlyShownThread.flatten(), 77 + shouldFetchMoreData = false, 78 + isShowingThread = true, 79 + ) 88 80 } 89 81 } 90 82 } 91 - } 92 - 83 + }
+13 -3
app/src/main/java/industries/geesawra/monarch/datalayer/Models.kt
··· 635 635 ) 636 636 637 637 data class ThreadPost( 638 - val post: SkeetData, 639 - val replies: List<ThreadPost> 640 - ) 638 + val post: SkeetData = SkeetData(), 639 + val level: Int = 0, 640 + val replies: List<ThreadPost> = listOf() 641 + ) { 642 + fun flatten(): List<SkeetData> { 643 + val list = mutableListOf<SkeetData>() 644 + list.add(post.copy(nestingLevel = level)) 645 + replies.forEach { reply -> 646 + list.addAll(reply.flatten()) 647 + } 648 + return list 649 + } 650 + }
+37 -59
app/src/main/java/industries/geesawra/monarch/datalayer/TimelineViewModel.kt
··· 56 56 57 57 val cidInteractedWith: Map<Cid, RKey> = mapOf(), 58 58 59 - val currentlyShownThread: List<List<SkeetData>> = listOf(), 59 + val currentlyShownThread: ThreadPost = ThreadPost(), 60 60 61 61 val loginError: String? = null, 62 62 val error: String? = null, ··· 535 535 } 536 536 537 537 fun setThread(tappedElement: SkeetData) { 538 - uiState = uiState.copy(currentlyShownThread = listOf(listOf(tappedElement))) 538 + uiState = uiState.copy(currentlyShownThread = ThreadPost(post = tappedElement)) 539 539 } 540 540 541 - private fun getAllThreads( 542 - thread: GetPostThreadResponseThreadUnion, 543 - startingLevel: Int = 0 544 - ): List<List<SkeetData>> { 545 - if (thread !is GetPostThreadResponseThreadUnion.ThreadViewPost) { 546 - return when (thread) { 547 - is GetPostThreadResponseThreadUnion.BlockedPost -> listOf(listOf(SkeetData(blocked = true))) 548 - is GetPostThreadResponseThreadUnion.NotFoundPost -> listOf(listOf(SkeetData(notFound = true))) 549 - else -> emptyList() 550 - } 551 - } 552 - 553 - val rootSkeet = SkeetData.fromPostView(thread.value.post, thread.value.post.author) 554 - val threads = mutableListOf<List<SkeetData>>() 555 - 556 - // fun findPaths(current: ThreadViewPostReplieUnion, currentPath: List<SkeetData>, level: Int) { 557 - // val skeet = when (current) { 558 - // is ThreadViewPostReplieUnion.ThreadViewPost -> SkeetData.fromPostView( 559 - // current.value.post, 560 - // current.value.post.author 561 - // ).copy(nestingLevel = level) 562 - // 563 - // is ThreadViewPostReplieUnion.BlockedPost -> SkeetData(blocked = true) 564 - // is ThreadViewPostReplieUnion.NotFoundPost -> SkeetData(notFound = true) 565 - // else -> null 566 - // } 567 - // 568 - // if (skeet == null) return 569 - // 570 - // val newPath = currentPath + skeet 571 - // val replies = (current as? ThreadViewPostReplieUnion.ThreadViewPost)?.value?.replies 572 - // 573 - // if (replies.isNullOrEmpty()) { 574 - // threads.add(newPath) 575 - // } else { 576 - // replies.forEach { findPaths(it, newPath, level + 1) } 577 - // } 578 - // } 579 - // 580 - 581 - val list = mutableListOf<SkeetData>() 541 + private fun readThread( 542 + threadUnion: GetPostThreadResponseThreadUnion, 543 + level: Int = 0 544 + ): ThreadPost { 545 + if (threadUnion !is GetPostThreadResponseThreadUnion.ThreadViewPost) { 546 + return when (threadUnion) { 547 + is GetPostThreadResponseThreadUnion.BlockedPost -> ThreadPost( 548 + post = SkeetData(blocked = true), 549 + level = level 550 + ) 582 551 583 - thread.value.replies.forEach { reply -> 584 - when (reply) { 585 - is ThreadViewPostReplieUnion.BlockedPost -> listOf(listOf(SkeetData(blocked = true))) 586 - is ThreadViewPostReplieUnion.NotFoundPost -> listOf(listOf(SkeetData(notFound = true))) 587 - is ThreadViewPostReplieUnion.ThreadViewPost -> run { 588 - list.add(SkeetData.fromPostView(reply.value.post, reply.value.post.author)) 589 - threads += getAllThreads( 590 - GetPostThreadResponseThreadUnion.ThreadViewPost(reply.value), 591 - startingLevel + 1 592 - ) 593 - } 552 + is GetPostThreadResponseThreadUnion.NotFoundPost -> ThreadPost( 553 + post = SkeetData(notFound = true), 554 + level = level 555 + ) 594 556 595 - is ThreadViewPostReplieUnion.Unknown -> {} 557 + else -> ThreadPost(level = level) // Default for unknown 596 558 } 597 559 } 598 560 561 + val currentPostSkeetData = SkeetData.fromPostView(threadUnion.value.post, threadUnion.value.post.author) 599 562 600 - threads.add(list) 563 + val replies = threadUnion.value.replies.map { replyUnion -> 564 + readThread( 565 + 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) 570 + }, 571 + level = level + 1 572 + ) 573 + } 601 574 602 - return threads.map { it.sortedBy { skeet -> skeet.createdAt } } 575 + return ThreadPost( 576 + post = currentPostSkeetData, 577 + level = level, 578 + replies = replies 579 + ) 603 580 } 604 581 605 582 fun getThread(then: () -> Unit) { 606 583 viewModelScope.launch { 607 - bskyConn.getThread(uiState.currentlyShownThread.first().first().uri).onFailure { 584 + bskyConn.getThread(uiState.currentlyShownThread.post.uri).onFailure { 608 585 uiState = when (it) { 609 586 is LoginException -> uiState.copy(loginError = it.message) 610 587 else -> uiState.copy(error = it.message) 611 588 } 612 589 }.onSuccess { 590 + val asd = readThread(it.thread) 613 591 uiState = uiState.copy( 614 - currentlyShownThread = getAllThreads(it.thread) 592 + currentlyShownThread = asd 615 593 ) 616 594 then() 617 595 }