mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-15 09:57:04 +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
|
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.database.local.PostComments
|
||||||
import dev.msfjarvis.claw.model.Comment
|
import dev.msfjarvis.claw.model.Comment
|
||||||
|
|
||||||
|
@ -29,15 +27,6 @@ internal data class CommentNode(
|
||||||
children.lastOrNull()?.addChild(child)
|
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(
|
internal fun createListNode(
|
||||||
|
@ -68,60 +57,5 @@ internal fun createListNode(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return commentNodes
|
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
|
package dev.msfjarvis.claw.common.comments
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.toMutableStateList
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.coroutines.runSuspendCatching
|
||||||
import com.github.michaelbull.result.fold
|
import com.github.michaelbull.result.fold
|
||||||
import dev.msfjarvis.claw.common.NetworkState
|
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.Comment
|
||||||
import dev.msfjarvis.claw.model.UIPost
|
import dev.msfjarvis.claw.model.UIPost
|
||||||
|
|
||||||
@Suppress("LongParameterList")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@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")
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CommentsPage(
|
fun CommentsPage(
|
||||||
postId: String,
|
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