Skip to content

Commit

Permalink
Liking Comments
Browse files Browse the repository at this point in the history
This diff adds the ability to like arbitrary comments on posts
  • Loading branch information
Rahkeen committed Jul 23, 2024
1 parent d315a62 commit 3c85d13
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 3c85d13

Please sign in to comment.