mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 22:17:03 +05:30
refactor(common): use a simpler tree view for comments
This commit is contained in:
parent
789e4a124d
commit
6223198191
3 changed files with 191 additions and 155 deletions
|
@ -6,8 +6,6 @@
|
|||
*/
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListScope
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
|
||||
|
@ -29,15 +27,6 @@ internal data class CommentNode(
|
|||
children.lastOrNull()?.addChild(child)
|
||||
}
|
||||
}
|
||||
|
||||
fun setExpanded(expanded: Boolean): CommentNode {
|
||||
this.isExpanded = expanded
|
||||
|
||||
if (children.isNotEmpty()) {
|
||||
children.forEach { it.setExpanded(expanded) }
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createListNode(
|
||||
|
@ -68,60 +57,5 @@ internal fun createListNode(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return commentNodes
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,27 +6,13 @@
|
|||
*/
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import android.widget.Toast
|
||||
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
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.michaelbull.result.coroutines.runSuspendCatching
|
||||
import com.github.michaelbull.result.fold
|
||||
import dev.msfjarvis.claw.common.NetworkState
|
||||
|
@ -40,81 +26,7 @@ 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,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val commentNodes = createListNode(details.comments, commentState).toMutableStateList()
|
||||
LaunchedEffect(key1 = commentNodes) {
|
||||
if (details.comments.isNotEmpty() && !commentState?.commentIds.isNullOrEmpty()) {
|
||||
val unreadCount = details.comments.size - (commentState?.commentIds?.size ?: 0)
|
||||
if (unreadCount > 0) {
|
||||
val text = "$unreadCount unread comments"
|
||||
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
markSeenComments(details.shortId, details.comments)
|
||||
}
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item {
|
||||
CommentsHeader(
|
||||
post = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
}
|
||||
|
||||
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 = node.setExpanded(!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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "LongParameterList")
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
fun CommentsPage(
|
||||
postId: String,
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright © 2021-2024 Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.common.comments
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.claw.common.posts.PostActions
|
||||
import dev.msfjarvis.claw.common.posts.Submitter
|
||||
import dev.msfjarvis.claw.common.ui.ThemedRichText
|
||||
import dev.msfjarvis.claw.database.local.PostComments
|
||||
import dev.msfjarvis.claw.model.Comment
|
||||
import dev.msfjarvis.claw.model.UIPost
|
||||
|
||||
@Composable
|
||||
internal fun CommentsPageInternal(
|
||||
details: UIPost,
|
||||
postActions: PostActions,
|
||||
htmlConverter: HTMLConverter,
|
||||
commentState: PostComments?,
|
||||
markSeenComments: (String, List<Comment>) -> Unit,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val commentNodes = createListNode(details.comments, commentState)
|
||||
LaunchedEffect(key1 = commentNodes) {
|
||||
if (details.comments.isNotEmpty() && !commentState?.commentIds.isNullOrEmpty()) {
|
||||
val unreadCount = details.comments.size - (commentState?.commentIds?.size ?: 0)
|
||||
if (unreadCount > 0) {
|
||||
val text = "$unreadCount unread comments"
|
||||
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
markSeenComments(details.shortId, details.comments)
|
||||
}
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
|
||||
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
|
||||
item {
|
||||
CommentsHeader(
|
||||
post = details,
|
||||
postActions = postActions,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile = openUserProfile,
|
||||
)
|
||||
}
|
||||
|
||||
if (commentNodes.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Comments",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
commentNodes.forEach { node -> item { Node(node, htmlConverter, openUserProfile) } }
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
text = "No Comments",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple tree view implementation by Anton Shilov who was smarter in 2020 than I am today
|
||||
* https://gist.github.com/antonshilov/ef8cd0a360a5cc0f823b2a4e85084720
|
||||
*/
|
||||
@Composable
|
||||
private fun Node(
|
||||
node: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
var isChildrenShown by remember { mutableStateOf(true) }
|
||||
|
||||
NodeBox(
|
||||
node = node,
|
||||
isExpanded = isChildrenShown,
|
||||
htmlConverter = htmlConverter,
|
||||
openUserProfile,
|
||||
modifier = modifier.clickable(onClick = { isChildrenShown = !isChildrenShown }),
|
||||
)
|
||||
|
||||
if (isChildrenShown) {
|
||||
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
node.children.forEach { model -> Node(model, htmlConverter, openUserProfile) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NodeBox(
|
||||
node: CommentNode,
|
||||
isExpanded: Boolean,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
CommentEntry(isExpanded, node, htmlConverter, openUserProfile, modifier)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
private val CommentEntryPadding = 16f.dp
|
||||
|
||||
@Composable
|
||||
private fun CommentEntry(
|
||||
isExpanded: Boolean,
|
||||
commentNode: CommentNode,
|
||||
htmlConverter: HTMLConverter,
|
||||
openUserProfile: (String) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val comment = commentNode.comment
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
if (commentNode.isUnread) MaterialTheme.colorScheme.surfaceContainerHigh
|
||||
else MaterialTheme.colorScheme.background
|
||||
)
|
||||
.padding(
|
||||
start = CommentEntryPadding * commentNode.indentLevel,
|
||||
end = CommentEntryPadding,
|
||||
top = CommentEntryPadding,
|
||||
bottom = CommentEntryPadding,
|
||||
)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Submitter(
|
||||
text =
|
||||
buildCommenterString(
|
||||
commenterName = comment.user,
|
||||
score = comment.score,
|
||||
createdAt = comment.createdAt,
|
||||
updatedAt = comment.updatedAt,
|
||||
),
|
||||
avatarUrl = "https://lobste.rs/avatars/${comment.user}-100.png",
|
||||
contentDescription = "User avatar for ${comment.user}",
|
||||
modifier = Modifier.clickable { openUserProfile(comment.user) },
|
||||
)
|
||||
if (isExpanded) {
|
||||
ThemedRichText(
|
||||
text = htmlConverter.convertHTMLToMarkdown(comment.comment),
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue