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 392024c3..feff7b0d 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,15 +6,15 @@ */ package dev.msfjarvis.claw.common.comments -import dev.msfjarvis.claw.database.local.PostComments import dev.msfjarvis.claw.model.Comment -internal class CommentNode( +internal data class CommentNode( val comment: Comment, private var parent: CommentNode? = null, val children: MutableList = mutableListOf(), val isUnread: Boolean = false, val indentLevel: Int, + val isExpanded: Boolean = true, ) { fun addChild(child: CommentNode) { @@ -50,6 +50,7 @@ internal class CommentNode( if (children != other.children) return false if (isUnread != other.isUnread) return false if (indentLevel != other.indentLevel) return false + if (isExpanded != other.isExpanded) return false return true } @@ -59,37 +60,7 @@ internal class CommentNode( result = 31 * result + children.hashCode() result = 31 * result + isUnread.hashCode() result = 31 * result + indentLevel + result = 31 * result + isExpanded.hashCode() return result } } - -internal fun createListNode( - comments: List, - commentState: PostComments?, -): MutableList { - val commentNodes = mutableListOf() - val isUnread = { id: String -> commentState?.commentIds?.contains(id) == false } - - for (i in comments.indices) { - if (comments[i].parentComment == null) { - commentNodes.add( - CommentNode( - comment = comments[i], - isUnread = isUnread(comments[i].shortId), - indentLevel = 1, - ) - ) - } else { - commentNodes.lastOrNull()?.let { commentNode -> - commentNode.addChild( - CommentNode( - comment = comments[i], - isUnread = isUnread(comments[i].shortId), - indentLevel = commentNode.indentLevel + 1, - ) - ) - } - } - } - return commentNodes -} diff --git a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsHandler.kt b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsHandler.kt new file mode 100644 index 00000000..eef9a0b6 --- /dev/null +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsHandler.kt @@ -0,0 +1,60 @@ +/* + * Copyright © 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 dev.msfjarvis.claw.database.local.PostComments +import dev.msfjarvis.claw.model.Comment +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +internal class CommentsHandler { + + private val _listItems: MutableStateFlow> = MutableStateFlow(emptyList()) + val listItems: StateFlow> = _listItems.asStateFlow() + + fun createListNode(comments: List, commentState: PostComments?) { + val commentNodes = mutableListOf() + val isUnread = { id: String -> commentState?.commentIds?.contains(id) == false } + + for (i in comments.indices) { + if (comments[i].parentComment == null) { + commentNodes.add( + CommentNode( + comment = comments[i], + isUnread = isUnread(comments[i].shortId), + indentLevel = 1, + ) + ) + } else { + commentNodes.lastOrNull()?.let { commentNode -> + commentNode.addChild( + CommentNode( + comment = comments[i], + isUnread = isUnread(comments[i].shortId), + indentLevel = commentNode.indentLevel + 1, + ) + ) + } + } + } + + _listItems.value = commentNodes + } + + fun updateListNode(shortId: String, isExpanded: Boolean) { + val listNode = _listItems.value.toMutableList() + val index = listNode.indexOfFirst { it.comment.shortId == shortId } + + if (index != -1) { + val commentNode = listNode[index].copy(isExpanded = isExpanded) + listNode[index] = commentNode + + _listItems.value = listNode.toList() + } + } +} 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 5853d753..ef1a7535 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 @@ -60,6 +60,7 @@ fun CommentsPage( markSeenComments = markSeenComments, openUserProfile = openUserProfile, contentPadding = contentPadding, + commentsHandler = CommentsHandler(), modifier = modifier.fillMaxSize(), ) } 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 index f9cff833..bb923b75 100644 --- a/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt +++ b/common/src/main/kotlin/dev/msfjarvis/claw/common/comments/CommentsPageImpl.kt @@ -8,6 +8,12 @@ package dev.msfjarvis.claw.common.comments import android.text.format.DateUtils import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -21,6 +27,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -28,9 +35,6 @@ 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 @@ -39,6 +43,7 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.msfjarvis.claw.common.posts.PostActions import dev.msfjarvis.claw.common.posts.Submitter import dev.msfjarvis.claw.common.ui.ThemedRichText @@ -48,6 +53,8 @@ import dev.msfjarvis.claw.model.UIPost import java.time.Instant import java.time.temporal.TemporalAccessor +private const val AnimationDuration = 100 + @Composable internal fun CommentsPageInternal( details: UIPost, @@ -57,10 +64,19 @@ internal fun CommentsPageInternal( markSeenComments: (String, List) -> Unit, openUserProfile: (String) -> Unit, contentPadding: PaddingValues, + commentsHandler: CommentsHandler, modifier: Modifier = Modifier, ) { + + LaunchedEffect(Unit) { commentsHandler.createListNode(details.comments, commentState) } + + val onToggleExpandedState = { shortId: String, isExpanded: Boolean -> + commentsHandler.updateListNode(shortId, isExpanded) + } + val context = LocalContext.current - val commentNodes = createListNode(details.comments, commentState) + val commentNodes by commentsHandler.listItems.collectAsStateWithLifecycle() + LaunchedEffect(key1 = commentNodes) { if (details.comments.isNotEmpty() && !commentState?.commentIds.isNullOrEmpty()) { val unreadCount = details.comments.size - (commentState?.commentIds?.size ?: 0) @@ -96,8 +112,8 @@ internal fun CommentsPageInternal( ) } - commentNodes.forEach { node -> - item(key = node.comment.shortId) { Node(node, htmlConverter, openUserProfile) } + items(items = commentNodes, key = { node -> node.comment.shortId }) { node -> + Node(node, htmlConverter, openUserProfile, onToggleExpandedState) } item(key = "bottom_spacer") { @@ -127,22 +143,30 @@ private fun Node( node: CommentNode, htmlConverter: HTMLConverter, openUserProfile: (String) -> Unit, + onToggleExpandedState: (String, Boolean) -> Unit, modifier: Modifier = Modifier, ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - var isChildrenShown by remember { mutableStateOf(true) } - NodeBox( node = node, - isExpanded = isChildrenShown, + isExpanded = node.isExpanded, htmlConverter = htmlConverter, openUserProfile, - modifier = modifier.clickable(onClick = { isChildrenShown = !isChildrenShown }), + modifier = + modifier.clickable( + onClick = { onToggleExpandedState(node.comment.shortId, !node.isExpanded) } + ), ) - if (isChildrenShown) { + AnimatedVisibility( + visible = node.isExpanded, + enter = fadeIn(tween(AnimationDuration)) + expandVertically(tween(AnimationDuration)), + exit = fadeOut(tween(AnimationDuration)) + shrinkVertically(tween(AnimationDuration)), + ) { Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { - node.children.forEach { model -> Node(model, htmlConverter, openUserProfile) } + node.children.forEach { model -> + Node(model, htmlConverter, openUserProfile, onToggleExpandedState) + } } } }