refactor(common): migrate comments page to Bonsai

This commit is contained in:
Harsh Shandilya 2024-05-04 20:10:47 +05:30
parent 5687e95455
commit 1835056176
3 changed files with 75 additions and 166 deletions

View File

@ -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(

View File

@ -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<CommentNode> = 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<Comment>,
commentState: PostComments?,
): MutableList<CommentNode> {
val commentNodes = mutableListOf<CommentNode>()
): ImmutableList<CommentNode> {
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<CommentNode>,
htmlConverter: HTMLConverter,
openUserProfile: (String) -> Unit,
): Tree<CommentNode> {
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<CommentNode>,
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) }
}
}

View File

@ -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,