Skip to content

Commit

Permalink
Posting Comments (#83)
Browse files Browse the repository at this point in the history
This PR is our initial pass at setting up comments (top level comments,
not replies yet)

Added a minimal UI for submitting comments
Modified the `CommentsDomain` with new state and actions for updating /
submitting comments
Extracted out Form Data neccesary for submitting comments
Added a post request to the comment endpoint to tie it all together
  • Loading branch information
Rahkeen authored Jul 26, 2024
1 parent 40c0f4b commit 899a57d
Show file tree
Hide file tree
Showing 8 changed files with 408 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fun App() {
startDestination = Stories
) {
storiesGraph(navController)
commentsRoutes()
commentsRoutes(navController)
bookmarksRoutes(navController)
settingsRoutes(navController)
loginRoutes(navController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.jsonObject
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Retrofit
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,42 @@
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"
private const val COMMENT_URL = BASE_WEB_URL + "comment"

data class ItemPage(
data class PostPage(
val postInfo: PostInfo,
val commentInfoMap: Map<Long, CommentInfo>,
val commentFormData: CommentFormData?
)

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

data class CommentPage(
data class CommentInfo(
val id: Long,
val upvoted: Boolean,
val upvoteUrl: String
)

data class CommentFormData(
val parentId: String,
val gotoUrl: String,
val hmac: String,
)

enum class LoginResponse {
Success,
Failed
Expand Down Expand Up @@ -64,9 +72,9 @@ class HackerNewsWebClient(
}
}
}
suspend fun getItemPage(itemId: Long): ItemPage {

suspend fun getPostPage(itemId: Long): PostPage {
return withContext(Dispatchers.IO) {
// request page
val response = httpClient.newCall(
Request
.Builder()
Expand All @@ -75,30 +83,57 @@ class HackerNewsWebClient(
).execute()

val document = Jsoup.parse(response.body?.string()!!)
val upvoteElement = document.select("#up_$itemId")
val upvoteHref = upvoteElement.attr("href")
val itemPageInfo = document.postInfo(itemId)
val commentPageInfoMap = document.commentInfos()
val addCommentFormData = document.commentFormData()

PostPage(
postInfo = itemPageInfo,
commentInfoMap = commentPageInfoMap,
commentFormData = addCommentFormData
)
}
}

private fun Document.postInfo(itemId: Long): PostInfo {
val upvoteElement = select("#up_$itemId")
return PostInfo(
id = itemId,
upvoted = upvoteElement.hasClass("nosee"),
upvoteUrl = BASE_WEB_URL + upvoteElement.attr("href")
)
}

val commentTree = document.select("table.comment-tree")
val commentUpvoteLinks = commentTree.select("a[id^=up_]")
val commentUpvoteMap = commentUpvoteLinks.groupBy(
private fun Document.commentInfos(): Map<Long, CommentInfo> {
val commentTree = select("table.comment-tree")
val commentUpvoteLinks = commentTree.select("a[id^=up_]")
return commentUpvoteLinks
.groupBy(
keySelector = { it.id().substring(3).toLong() },
valueTransform = {
CommentPage(
CommentInfo(
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,
commentUrlMap = commentUpvoteMap
)
private fun Document.commentFormData(): CommentFormData? {
val commentFormElement = select("form[action=comment]")
if (commentFormElement.isEmpty()) {
return null
}

val parentId = commentFormElement.select("input[name=parent]").attr("value")
val goto = commentFormElement.select("input[name=goto]").attr("value")
val hmac = commentFormElement.select("input[name=hmac]").attr("value")
return CommentFormData(
parentId = parentId,
gotoUrl = goto,
hmac = hmac,
)
}

suspend fun upvoteItem(url: String): Boolean {
Expand All @@ -113,4 +148,28 @@ class HackerNewsWebClient(
}
}

suspend fun postComment(
parentId: String,
gotoUrl: String,
hmac: String,
text: String
): Boolean {
return withContext(Dispatchers.IO) {
val response = httpClient.newCall(
Request.Builder()
.url(COMMENT_URL)
.post(
FormBody.Builder()
.add("parent", parentId)
.add("goto", gotoUrl)
.add("hmac", hmac)
.add("text", text)
.build()
)
.build()
).execute()

response.isSuccessful
}
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
package com.emergetools.hackernews.data

import android.util.Log
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl

class LocalCookieJar(private val userStorage: UserStorage): CookieJar {
class LocalCookieJar(private val userStorage: UserStorage) : CookieJar {

override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
Log.d("Cookie Jar", "Url: $url, cookie = ${cookies[0]}")
cookies.firstOrNull { it.name == "user" }?.let { authCookie ->
runBlocking { userStorage.saveCookie(authCookie.value) }
}
}

override fun loadForRequest(url: HttpUrl): List<Cookie> {
val authCookie = runBlocking { userStorage.getCookie().first() }
Log.d("Cookie Jar", "Cookie: user=$authCookie" )
return if (authCookie != null) {
val cookie = Cookie.Builder()
.name("user")
Expand Down
Loading

0 comments on commit 899a57d

Please sign in to comment.