Skip to content

Commit

Permalink
Comment Hiding + HTML Comments
Browse files Browse the repository at this point in the history
This PR adds the ability to hide comment "subtrees".

Because of the way we parse, each comment is an indpendent entity with no parent-child relationship. However, we can use the comment order + indent level to determine if a comment is a child of some arbitrary parent comment.
I setup a simple function to find all the children of a comment that would be hidden or displayed based on the selected parent.
I also added a lil icon to show show if a comment is displayed or hidden.

Last thing I snuck in here was getting the comment HTML rather than the escaped string, so that we can parse comments as HTML (render links, formatting, etc).
  • Loading branch information
Rahkeen committed Aug 8, 2024
1 parent 68e86e2 commit 4b1b7e7
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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_]")
Expand Down Expand Up @@ -206,4 +206,4 @@ class HackerNewsWebClient(
document.commentInfos()
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CommentState>
val hidden: HiddenStatus

data class Loading(override val level: Int) : CommentState {
override val children: List<CommentState> = emptyList()
override val hidden: HiddenStatus = HiddenStatus.Displayed
}

data class Content(
Expand All @@ -96,6 +118,7 @@ sealed interface CommentState {
val upvoted: Boolean,
val upvoteUrl: String,
override val children: List<CommentState>,
override val hidden: HiddenStatus = HiddenStatus.Displayed,
override val level: Int = 0,
) : CommentState
}
Expand Down Expand Up @@ -134,6 +157,8 @@ sealed interface CommentsAction {
val hmac: String,
val text: String
) : CommentsAction

data class ToggleHideComment(val id: Long) : CommentsAction
}

sealed interface CommentsNavigation {
Expand Down Expand Up @@ -265,6 +290,48 @@ class CommentsViewModel(
}
}
}

is CommentsAction.ToggleHideComment -> {
fun toggleComments(
parentId: Long,
comments: List<CommentState.Content>
): List<CommentState.Content> {
val updates = mutableListOf<CommentState.Content>()

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<CommentState.Content>()
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)
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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),
Expand All @@ -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
Expand Down Expand Up @@ -299,7 +315,7 @@ fun CommentRow(
)
}
}
Row {
if (state.hidden == HiddenStatus.Displayed) {
Text(
text = state.content.parseAsHtml(),
style = MaterialTheme.typography.labelSmall,
Expand All @@ -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
Expand Down Expand Up @@ -388,13 +414,6 @@ fun CommentRow(
}
}
}
state.children.forEach { child ->
CommentRow(
modifier = modifier,
state = child,
onLikeTapped = onLikeTapped
)
}
}

@PreviewLightDark
Expand All @@ -413,6 +432,7 @@ fun CommentRowPreview() {
upvoteUrl = "",
children = listOf()
),
onToggleHide = {},
onLikeTapped = {}
)
}
Expand All @@ -426,6 +446,7 @@ fun CommentRowLoadingPreview() {
Column {
CommentRow(
state = CommentState.Loading(level = 0),
onToggleHide = {},
onLikeTapped = {}
)
}
Expand Down
10 changes: 10 additions & 0 deletions android/app/src/main/res/drawable/ic_collapse.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M5.23,15.791C4.932,15.503 4.922,15.029 5.209,14.73L9.459,10.23C9.601,10.083 9.796,10 10,10C10.204,10 10.399,10.083 10.541,10.23L14.791,14.73C15.078,15.029 15.068,15.503 14.77,15.791C14.471,16.078 13.997,16.068 13.709,15.77L10,11.832L6.291,15.77C6.004,16.068 5.529,16.078 5.23,15.791ZM5.23,9.791C4.932,9.504 4.922,9.029 5.209,8.73L9.459,4.23C9.601,4.083 9.796,4 10,4C10.204,4 10.399,4.083 10.541,4.23L14.791,8.73C15.078,9.029 15.068,9.504 14.77,9.791C14.471,10.078 13.997,10.068 13.709,9.77L10,5.832L6.291,9.77C6.004,10.068 5.529,10.078 5.23,9.791Z"
android:fillColor="#0F172A"
android:fillType="evenOdd"/>
</vector>

0 comments on commit 4b1b7e7

Please sign in to comment.