Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Pass at Liking Comments #80

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading