diff --git a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt b/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt index 34b06148..8fbe5045 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/data/HackerNewsWebClient.kt @@ -132,7 +132,7 @@ class HackerNewsWebClient( val infos = comments.map { commentElement -> val id = commentElement.id().toLong() val level = commentElement.select("td.ind").attr("indent").toInt() - val text = commentElement.select("div.commtext").text() + val text = commentElement.select("div.commtext").html() val user = commentElement.select("a.hnuser").text() val time = commentElement.select("span.age").attr("title") val upvoteLink = commentElement.select("a[id^=up_]") @@ -206,4 +206,4 @@ class HackerNewsWebClient( document.commentInfos() } } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt index 742387af..f9c691f8 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsDomain.kt @@ -80,12 +80,34 @@ data class PostCommentState( val text: String, ) +enum class HiddenStatus { + Hidden, + HiddenByParent, + Displayed; + + fun toggle(): HiddenStatus { + return when(this) { + Hidden, HiddenByParent -> Displayed + else -> Hidden + } + } + + fun toggleChild(): HiddenStatus { + return when(this) { + Hidden, HiddenByParent -> Displayed + else -> HiddenByParent + } + } +} + sealed interface CommentState { val level: Int val children: List + val hidden: HiddenStatus data class Loading(override val level: Int) : CommentState { override val children: List = emptyList() + override val hidden: HiddenStatus = HiddenStatus.Displayed } data class Content( @@ -96,6 +118,7 @@ sealed interface CommentState { val upvoted: Boolean, val upvoteUrl: String, override val children: List, + override val hidden: HiddenStatus = HiddenStatus.Displayed, override val level: Int = 0, ) : CommentState } @@ -134,6 +157,8 @@ sealed interface CommentsAction { val hmac: String, val text: String ) : CommentsAction + + data class ToggleHideComment(val id: Long) : CommentsAction } sealed interface CommentsNavigation { @@ -265,6 +290,48 @@ class CommentsViewModel( } } } + + is CommentsAction.ToggleHideComment -> { + fun toggleComments( + parentId: Long, + comments: List + ): List { + val updates = mutableListOf() + + val parentIndex = comments.indexOfFirst { it.id == parentId } + val parentComment = comments[parentIndex] + updates.add(parentComment.copy(hidden = parentComment.hidden.toggle())) + + val parentLevel = parentComment.level + var currentIndex = parentIndex + 1 + while (currentIndex <= comments.lastIndex) { + val currentChild = comments[currentIndex] + if (currentChild.level <= parentLevel) { + break + } + updates.add( + currentChild.copy( + hidden = parentComment.hidden.toggleChild() + ) + ) + currentIndex++ + } + return updates + } + + val currentState = internalState.value + if (currentState is CommentsState.Content) { + val contentComments = currentState.comments.filterIsInstance() + val updates = toggleComments(action.id, contentComments) + val updatedState = currentState.copy( + comments = contentComments.map { prev -> + updates.find { it.id == prev.id } ?: prev + } + ) + + internalState.compareAndSet(currentState, updatedState) + } + } } } diff --git a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt index b551dc30..860e4dca 100644 --- a/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt +++ b/android/app/src/main/java/com/emergetools/hackernews/features/comments/CommentsScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.geometry.center import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight @@ -121,9 +122,12 @@ fun CommentsScreen( } ) } - items(items = state.comments) { comment -> + items(items = state.comments.filter { it.hidden != HiddenStatus.HiddenByParent }) { comment -> CommentRow( state = comment, + onToggleHide = { + actions(CommentsAction.ToggleHideComment(it.id)) + }, onLikeTapped = { if (state is CommentsState.Content && state.loggedIn) { actions( @@ -235,21 +239,23 @@ private fun CommentsScreenLoadingPreview() { fun CommentRow( modifier: Modifier = Modifier, state: CommentState, + onToggleHide: (CommentState.Content) -> Unit, onLikeTapped: (CommentState.Content) -> Unit ) { - val startPadding = (state.level * 16).dp - Column( - modifier = modifier - .padding(start = startPadding) - .fillMaxWidth() - .heightIn(min = 80.dp) - .clip(RoundedCornerShape(8.dp)) - .background(color = MaterialTheme.colorScheme.surface) - .padding(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - when (state) { - is CommentState.Content -> { + when (state) { + is CommentState.Content -> { + val startPadding = (state.level * 16).dp + Column( + modifier = modifier + .padding(start = startPadding) + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(8.dp)) + .clickable { onToggleHide(state) } + .background(color = MaterialTheme.colorScheme.surface) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -268,9 +274,19 @@ fun CommentRow( modifier = Modifier.size(12.dp), painter = painterResource(R.drawable.ic_time_outline), tint = MaterialTheme.colorScheme.onSurface, - contentDescription = "Time posted" + contentDescription = "Time Posted" ) } + Icon( + modifier = Modifier + .graphicsLayer { + rotationZ = if (state.hidden == HiddenStatus.Hidden) 180f else 0f + } + .size(12.dp), + painter = painterResource(R.drawable.ic_collapse), + tint = MaterialTheme.colorScheme.onSurface, + contentDescription = "Expand or Collapse" + ) Spacer(modifier = Modifier.weight(1f)) Box( modifier = Modifier @@ -299,7 +315,7 @@ fun CommentRow( ) } } - Row { + if (state.hidden == HiddenStatus.Displayed) { Text( text = state.content.parseAsHtml(), style = MaterialTheme.typography.labelSmall, @@ -308,18 +324,28 @@ fun CommentRow( ) } } + } - is CommentState.Loading -> { - val infiniteTransition = rememberInfiniteTransition("Skeleton") - val skeletonAlpha by infiniteTransition.animateFloat( - initialValue = 0.2f, - targetValue = 0.6f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis = 1000, easing = LinearEasing), - repeatMode = RepeatMode.Reverse, - ), - label = "Skeleton Alpha" - ) + is CommentState.Loading -> { + val infiniteTransition = rememberInfiniteTransition("Skeleton") + val skeletonAlpha by infiniteTransition.animateFloat( + initialValue = 0.2f, + targetValue = 0.6f, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis = 1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "Skeleton Alpha" + ) + Column( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .clip(RoundedCornerShape(8.dp)) + .background(color = MaterialTheme.colorScheme.surface) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically @@ -388,13 +414,6 @@ fun CommentRow( } } } - state.children.forEach { child -> - CommentRow( - modifier = modifier, - state = child, - onLikeTapped = onLikeTapped - ) - } } @PreviewLightDark @@ -413,6 +432,7 @@ fun CommentRowPreview() { upvoteUrl = "", children = listOf() ), + onToggleHide = {}, onLikeTapped = {} ) } @@ -426,6 +446,7 @@ fun CommentRowLoadingPreview() { Column { CommentRow( state = CommentState.Loading(level = 0), + onToggleHide = {}, onLikeTapped = {} ) } diff --git a/android/app/src/main/res/drawable/ic_collapse.xml b/android/app/src/main/res/drawable/ic_collapse.xml new file mode 100644 index 00000000..51bb7cbf --- /dev/null +++ b/android/app/src/main/res/drawable/ic_collapse.xml @@ -0,0 +1,10 @@ + + +