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 0a6227c8..0955ae9b 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,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, - 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, - ) - } -} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt index 7520c396..27d5da3e 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPage.kt @@ -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) -> 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, diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt new file mode 100644 index 00000000..78a0045f --- /dev/null +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt @@ -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) -> 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), + ) + } + } + } +}