From 1835056176077e88564b3b5f1651430921f90cb9 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sat, 4 May 2024 20:10:47 +0530 Subject: [PATCH] refactor(common): migrate comments page to Bonsai --- .../claw/common/comments/CommentEntry.kt | 14 +- .../claw/common/comments/CommentNode.kt | 141 ++++++------------ .../claw/common/comments/Comments.kt | 86 ++++------- 3 files changed, 75 insertions(+), 166 deletions(-) diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt index e7de53cd..5d08fb1f 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentEntry.kt @@ -124,29 +124,17 @@ private fun PostLink(linkMetadata: LinkMetadata, modifier: Modifier = Modifier) } } -private val CommentEntryPadding = 16f.dp - @Composable internal fun CommentEntry( commentNode: CommentNode, htmlConverter: HTMLConverter, - toggleExpanded: (CommentNode) -> Unit, openUserProfile: (String) -> Unit, modifier: Modifier = Modifier, ) { val comment = commentNode.comment Box( modifier = - modifier - .fillMaxWidth() - .clickable { toggleExpanded(commentNode) } - .background(MaterialTheme.colorScheme.background) - .padding( - start = CommentEntryPadding * commentNode.indentLevel, - end = CommentEntryPadding, - top = CommentEntryPadding, - bottom = CommentEntryPadding, - ) + modifier.fillMaxWidth().background(MaterialTheme.colorScheme.background).padding(16.dp) ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Submitter( diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt index 2ea9b398..89e13329 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentNode.kt @@ -6,121 +6,64 @@ */ package dev.msfjarvis.claw.common.comments -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.Composable +import dev.msfjarvis.claw.common.bonsai.node.Branch +import dev.msfjarvis.claw.common.bonsai.node.Leaf +import dev.msfjarvis.claw.common.bonsai.tree.Tree +import dev.msfjarvis.claw.common.bonsai.tree.TreeScope +import dev.msfjarvis.claw.common.bonsai.tree.tree import dev.msfjarvis.claw.database.local.PostComments import dev.msfjarvis.claw.model.Comment +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toPersistentList internal data class CommentNode( val comment: Comment, - var parent: CommentNode? = null, - val children: MutableList = mutableListOf(), val isUnread: Boolean = false, var isExpanded: Boolean = true, - var indentLevel: Int, -) { - fun addChild(child: CommentNode) { - if (comment.shortId == child.comment.parentComment) { - children.add(child) - child.parent = this - } else { - child.indentLevel += 1 - children.lastOrNull()?.addChild(child) - } - } -} +) -internal fun createListNode( +internal fun createCommentList( comments: List, commentState: PostComments?, -): MutableList { - val commentNodes = mutableListOf() +): ImmutableList { val isUnread = { id: String -> commentState?.commentIds?.contains(id) == false } + return comments + .map { comment -> CommentNode(comment, isUnread(comment.shortId), isExpanded = true) } + .toPersistentList() +} - for (i in comments.indices) { - if (comments[i].parentComment == null) { - commentNodes.add( - CommentNode( - comment = comments[i], - isUnread = isUnread(comments[i].shortId), - indentLevel = 1, - ) +@Composable +internal fun createTree( + comments: ImmutableList, + htmlConverter: HTMLConverter, + openUserProfile: (String) -> Unit, +): Tree { + val commentsMap = comments.groupBy { it.comment.parentComment } + + @Composable + fun TreeScope.buildTree(commentId: String?) { + val comment = comments.find { it.comment.shortId == commentId }!! + val children = commentsMap[commentId] + return if (children.isNullOrEmpty()) { + Leaf( + content = comment, + customIcon = {}, + customName = { CommentEntry(comment, htmlConverter, openUserProfile) }, ) } else { - commentNodes.lastOrNull()?.let { - it.addChild( - CommentNode( - comment = comments[i], - isUnread = isUnread(comments[i].shortId), - indentLevel = it.indentLevel + 1, - ) - ) - } + Branch( + content = comment, + children = { children.map { buildTree(it.comment.shortId) } }, + customIcon = {}, + customName = { CommentEntry(comment, htmlConverter, openUserProfile) }, + ) } } - return commentNodes -} - -internal fun setExpanded(commentNode: CommentNode, expanded: Boolean): CommentNode { - commentNode.isExpanded = expanded - - if (commentNode.children.isNotEmpty()) { - commentNode.children.forEach { setExpanded(it, expanded) } - } - return commentNode -} - -internal tailrec fun findTopMostParent(node: CommentNode): CommentNode { - val parent = node.parent - return if (parent != null) { - findTopMostParent(parent) - } else { - node - } -} - -internal fun LazyListScope.nodes( - nodes: List, - htmlConverter: HTMLConverter, - toggleExpanded: (CommentNode) -> Unit, - openUserProfile: (String) -> Unit, -) { - nodes.forEach { node -> - node( - node = node, - htmlConverter = htmlConverter, - toggleExpanded = toggleExpanded, - openUserProfile = openUserProfile, - ) - } -} - -private fun LazyListScope.node( - node: CommentNode, - htmlConverter: HTMLConverter, - toggleExpanded: (CommentNode) -> Unit, - openUserProfile: (String) -> Unit, -) { - // Skip the node if neither the node nor its parent is expanded - if (!node.isExpanded && node.parent?.isExpanded == false) { - return - } - item(key = node.comment.shortId) { - CommentEntry( - commentNode = node, - htmlConverter = htmlConverter, - toggleExpanded = toggleExpanded, - openUserProfile = openUserProfile, - ) - HorizontalDivider() - } - if (node.children.isNotEmpty()) { - nodes( - node.children, - htmlConverter = htmlConverter, - toggleExpanded = toggleExpanded, - openUserProfile = openUserProfile, - ) + return tree { + val rootCommentIds = + comments.filter { it.comment.parentComment == null }.map { it.comment.shortId } + rootCommentIds.map { buildTree(it) } } } diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt index b449e678..a3ae4930 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/Comments.kt @@ -9,9 +9,7 @@ package dev.msfjarvis.claw.common.comments import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -19,11 +17,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState -import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.unit.dp import com.github.michaelbull.result.coroutines.runSuspendCatching import com.github.michaelbull.result.fold @@ -31,6 +27,8 @@ import dev.msfjarvis.claw.common.NetworkState import dev.msfjarvis.claw.common.NetworkState.Error import dev.msfjarvis.claw.common.NetworkState.Loading import dev.msfjarvis.claw.common.NetworkState.Success +import dev.msfjarvis.claw.common.bonsai.Bonsai +import dev.msfjarvis.claw.common.bonsai.BonsaiStyle import dev.msfjarvis.claw.common.posts.PostActions import dev.msfjarvis.claw.common.ui.NetworkError import dev.msfjarvis.claw.common.ui.ProgressBar @@ -38,7 +36,6 @@ import dev.msfjarvis.claw.database.local.PostComments import dev.msfjarvis.claw.model.Comment import dev.msfjarvis.claw.model.UIPost -@Suppress("LongParameterList") @Composable private fun CommentsPageInternal( details: UIPost, @@ -49,60 +46,41 @@ private fun CommentsPageInternal( openUserProfile: (String) -> Unit, modifier: Modifier = Modifier, ) { - val commentNodes = createListNode(details.comments, commentState).toMutableStateList() - LaunchedEffect(key1 = commentNodes) { markSeenComments(details.shortId, details.comments) } + val tree = + createTree(createCommentList(details.comments, commentState), htmlConverter, openUserProfile) + LaunchedEffect(Unit) { + // Start off the tree in expanded state, then let Bonsai take care of it. + tree.expandAll() - Surface(color = MaterialTheme.colorScheme.surfaceVariant) { - LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) { - item { - CommentsHeader( - post = details, - postActions = postActions, - htmlConverter = htmlConverter, - openUserProfile = openUserProfile, - ) - } + markSeenComments(details.shortId, details.comments) + } - if (commentNodes.isNotEmpty()) { - item { - Text( - text = "Comments", - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), - ) - } - - nodes( - nodes = commentNodes, - htmlConverter = htmlConverter, - toggleExpanded = { node -> - val newNode = setExpanded(node, !node.isExpanded) - val parent = findTopMostParent(newNode) - val index = - commentNodes.indexOf(commentNodes.find { it.comment.url == parent.comment.url }) - if (index != -1) { - commentNodes.removeAt(index) - commentNodes.add(index, parent) - } - }, - openUserProfile = openUserProfile, - ) - } else { - item { - Text( - text = "No Comments", - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.fillMaxWidth().padding(16.dp), - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ) - } - } + Surface(color = MaterialTheme.colorScheme.surfaceVariant, modifier = modifier) { + Bonsai( + tree = tree, + style = + BonsaiStyle( + nodePadding = PaddingValues(0.dp), + useHorizontalScroll = false, + nodeShape = RectangleShape, + ), + ) { + CommentsHeader( + post = details, + postActions = postActions, + htmlConverter = htmlConverter, + openUserProfile = openUserProfile, + ) + Text( + text = "Comments", + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + ) } } } -@Suppress("UNCHECKED_CAST", "LongParameterList") +@Suppress("UNCHECKED_CAST") @Composable fun CommentsPage( postId: String,