Skip to content

Commit

Permalink
Initial Pass at Liking Comments (#80)
Browse files Browse the repository at this point in the history
This builds on the previously added login functionality to be able to
like comments.

This mostly gets the wiring in place, it is not the final state of the
UI, and we still need to think about how eager state updates will work
(essentially updating a tree as our backing state).
  • Loading branch information
Rahkeen authored Jul 25, 2024
1 parent 48bfc27 commit 2cdc6aa
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
package com.emergetools.hackernews.data

import android.util.Log
import androidx.compose.runtime.key
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.trimSubstring
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import org.jsoup.select.Elements

const val BASE_WEB_URL = "https://news.ycombinator.com/"
private const val LOGIN_URL = BASE_WEB_URL + "login"
private const val ITEM_URL = BASE_WEB_URL + "item"

data class ItemPage(
val id: Long,
val upvoted: Boolean,
val upvoteUrl: String,
val commentUrlMap: Map<Long, CommentPage>
)

data class CommentPage(
val id: Long,
val upvoted: Boolean,
val upvoteUrl: String
Expand Down Expand Up @@ -66,10 +78,25 @@ class HackerNewsWebClient(
val upvoteElement = document.select("#up_$itemId")
val upvoteHref = upvoteElement.attr("href")


val commentTree = document.select("table.comment-tree")
val commentUpvoteLinks = commentTree.select("a[id^=up_]")
val commentUpvoteMap = commentUpvoteLinks.groupBy(
keySelector = { it.id().substring(3).toLong() },
valueTransform = {
CommentPage(
id = it.id().substring(3).toLong(),
upvoted = it.hasClass("nosee"),
upvoteUrl = BASE_WEB_URL + it.attr("href")
)
}
).mapValues { it.value[0] }

ItemPage(
id = itemId,
upvoted = upvoteElement.hasClass("nosee"),
upvoteUrl = BASE_WEB_URL + upvoteHref
upvoteUrl = BASE_WEB_URL + upvoteHref,
commentUrlMap = commentUpvoteMap
)
}
}
Expand All @@ -85,4 +112,5 @@ class HackerNewsWebClient(
response.isSuccessful
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.emergetools.hackernews.data.CommentPage
import com.emergetools.hackernews.data.HackerNewsSearchClient
import com.emergetools.hackernews.data.HackerNewsWebClient
import com.emergetools.hackernews.data.ItemPage
Expand All @@ -15,6 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonNull.content
import java.time.OffsetDateTime

sealed interface CommentsState {
Expand Down Expand Up @@ -61,6 +63,8 @@ sealed interface CommentState {
val author: String,
val content: String,
val timeLabel: String,
val upvoted: Boolean,
val upvoteUrl: String,
override val children: List<CommentState>,
override val level: Int = 0,
): CommentState
Expand All @@ -78,7 +82,11 @@ sealed interface HeaderState {
}

sealed interface CommentsAction {
data object LikePostTapped: CommentsAction
data object LikePost: CommentsAction
data class LikeComment(
val id: Long,
val url: String
): CommentsAction
}

class CommentsViewModel(
Expand All @@ -89,14 +97,15 @@ class CommentsViewModel(
private val internalState = MutableStateFlow<CommentsState>(CommentsState.Loading)
val state = internalState.asStateFlow()


init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val response = searchClient.api.getItem(itemId)
val page = webClient.getItemPage(itemId)
Log.d("CommentsViewModel", "Item Page: $page")
val comments = response.children.map { rootComment ->
rootComment.createCommentState(0)
rootComment.createCommentState(0, page.commentUrlMap)
}
internalState.update {
CommentsState.Content(
Expand All @@ -115,7 +124,7 @@ class CommentsViewModel(

fun actions(action: CommentsAction) {
when (action) {
CommentsAction.LikePostTapped -> {
CommentsAction.LikePost -> {
Log.d("CommentsViewModel", "Post Liked: $itemId")
val current = internalState.value
if (current is CommentsState.Content && !current.page.upvoted && current.page.upvoteUrl.isNotEmpty()) {
Expand All @@ -135,22 +144,33 @@ class CommentsViewModel(
}
}
}

is CommentsAction.LikeComment -> {
viewModelScope.launch {
val success = webClient.upvoteItem(action.url)
if (success) {
Log.d("CommentsViewModel", "Liked Comment ${action.url}")
}
}
}
}
}

private fun ItemResponse.createCommentState(level: Int): CommentState {
private fun ItemResponse.createCommentState(level: Int, urlMap: Map<Long, CommentPage>): CommentState {
Log.d("Creating CommentState()", "Level: $level, Id: $id")

val page = urlMap[id]
return CommentState.Content(
id = id,
author = author ?: "",
content = text ?: "",
children = children.map { child ->
child.createCommentState(level + 1)
child.createCommentState(level + 1, urlMap)
},
timeLabel = relativeTimeStamp(
epochSeconds = OffsetDateTime.parse(createdAt).toInstant().epochSecond
),
upvoted = page?.upvoted ?: false,
upvoteUrl = page?.upvoteUrl.orEmpty(),
level = level
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ fun CommentsScreen(
.fillMaxWidth()
.wrapContentHeight(),
onLikeTapped = {
actions(CommentsAction.LikePostTapped)
actions(CommentsAction.LikePost)
}
)
}
Expand All @@ -89,7 +89,17 @@ fun CommentsScreen(
)
}
items(items = state.comments) { comment ->
CommentRow(comment)
CommentRow(
state = comment,
onLikeTapped = {
actions(
CommentsAction.LikeComment(
id = it.id,
url = it.upvoteUrl
)
)
}
)
}
}
}
Expand All @@ -108,7 +118,8 @@ private fun CommentsScreenPreview() {
page = ItemPage(
id = 0,
upvoted = false,
upvoteUrl = "upvote.com"
upvoteUrl = "upvote.com",
commentUrlMap = emptyMap()
),
comments = listOf(
CommentState.Content(
Expand All @@ -117,13 +128,17 @@ private fun CommentsScreenPreview() {
author = "rikinm",
content = "Hello Child",
timeLabel = "2d ago",
upvoted = false,
upvoteUrl = "",
children = listOf(
CommentState.Content(
id = 2,
level = 1,
author = "vasantm",
content = "Hello Parent",
timeLabel = "1h ago",
upvoted = false,
upvoteUrl = "",
children = listOf()
)
)
Expand All @@ -147,7 +162,7 @@ private fun CommentsScreenLoadingPreview() {
}

@Composable
fun CommentRow(state: CommentState) {
fun CommentRow(state: CommentState, onLikeTapped: (CommentState.Content) -> Unit) {
val startPadding = (state.level * 16).dp
Column(
modifier = Modifier
Expand Down Expand Up @@ -184,12 +199,22 @@ fun CommentRow(state: CommentState) {
color = Color.Gray
)
Spacer(modifier = Modifier.weight(1f))
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Default.ThumbUp,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = "upvote"
)
Box(
modifier = Modifier
.wrapContentSize()
.clip(CircleShape)
.background(color = MaterialTheme.colorScheme.surfaceContainerHighest)
.padding(vertical = 4.dp, horizontal = 8.dp)
.clickable { onLikeTapped(state) },
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier.size(12.dp),
imageVector = Icons.Default.ThumbUp,
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = "upvote"
)
}
}
Row {
Text(
Expand Down Expand Up @@ -248,7 +273,7 @@ fun CommentRow(state: CommentState) {
}
}
state.children.forEach { child ->
CommentRow(child)
CommentRow(state = child, onLikeTapped = onLikeTapped)
}
}

Expand All @@ -264,17 +289,22 @@ fun CommentRowPreview() {
author = "rikinm",
content = "Hello Parent",
timeLabel = "2d ago",
upvoted = false,
upvoteUrl = "",
children = listOf(
CommentState.Content(
id = 2,
level = 1,
author = "vasantm",
content = "Hello Child",
timeLabel = "2h ago",
upvoted = false,
upvoteUrl = "",
children = listOf()
)
)
)
),
onLikeTapped = {}
)
}
}
Expand All @@ -286,7 +316,8 @@ fun CommentRowLoadingPreview() {
HackerNewsTheme {
Column {
CommentRow(
state = CommentState.Loading(level = 0)
state = CommentState.Loading(level = 0),
onLikeTapped = {}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import com.emergetools.hackernews.ui.theme.HackerNewsTheme
Expand Down Expand Up @@ -63,6 +66,12 @@ fun LoginScreen(
TextField(
value = state.password,
placeholder = { Text("Password") },
visualTransformation = { text ->
TransformedText(
text = AnnotatedString("*".repeat(text.text.length)),
offsetMapping = OffsetMapping.Identity
)
},
trailingIcon = {
if (state.status == LoginStatus.Failed) {
Icon(
Expand Down

0 comments on commit 2cdc6aa

Please sign in to comment.