refactor(common): migrate comments page to Bonsai
This commit is contained in:
parent
5687e95455
commit
1835056176
|
@ -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(
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue