[1.549.*] Pre-release merge (#1172)
This commit is contained in:
+10
-21
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="6" by="lint 9.1.0" type="baseline" client="gradle" dependencies="true" name="AGP (9.1.0)" variant="all" version="9.1.0">
|
||||
<issues format="6" by="lint 9.2.1" type="baseline" client="gradle" dependencies="true" name="AGP (9.2.1)" variant="all" version="9.2.1">
|
||||
|
||||
<issue
|
||||
id="Instantiatable"
|
||||
@@ -19,7 +19,7 @@
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="64"
|
||||
line="69"
|
||||
column="15"/>
|
||||
</issue>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="64"
|
||||
line="69"
|
||||
column="35"/>
|
||||
</issue>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="73"
|
||||
line="78"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
errorLine2=" ~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="73"
|
||||
line="78"
|
||||
column="21"/>
|
||||
</issue>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="75"
|
||||
line="81"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="76"
|
||||
line="82"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="77"
|
||||
line="83"
|
||||
column="19"/>
|
||||
</issue>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="107"
|
||||
line="118"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
errorLine2=" ~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="107"
|
||||
line="118"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
@@ -122,15 +122,4 @@
|
||||
column="49"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SetJavaScriptEnabled"
|
||||
message="Using `setJavaScriptEnabled` can introduce XSS vulnerabilities into your application, review carefully"
|
||||
errorLine1=" settings.javaScriptEnabled = true"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/kotlin/dev/msfjarvis/claw/android/ui/screens/LoginScreen.kt"
|
||||
line="75"
|
||||
column="17"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
||||
|
||||
@@ -119,6 +119,7 @@ fun LobstersPostsScreen(
|
||||
|
||||
val filteredTags by tagFilterViewModel.filteredTags.collectAsStateWithLifecycle(persistentSetOf())
|
||||
val isLoggedIn by settingsViewModel.isLoggedIn.collectAsStateWithLifecycle(false)
|
||||
val username by settingsViewModel.username.collectAsStateWithLifecycle(null)
|
||||
|
||||
LaunchedEffect(deepLinkDestination) {
|
||||
if (deepLinkDestination != null) {
|
||||
@@ -283,6 +284,7 @@ fun LobstersPostsScreen(
|
||||
openLoginScreen = { navigateTo(backStack, Login) },
|
||||
onLogout = { settingsViewModel.logout() },
|
||||
isLoggedIn = isLoggedIn,
|
||||
username = username,
|
||||
importPosts = viewModel::importPosts,
|
||||
exportPostsAsJson = viewModel::exportPostsAsJson,
|
||||
exportPostsAsHtml = viewModel::exportPostsAsHtml,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.ui.screens
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.WebResourceError
|
||||
@@ -37,9 +38,13 @@ import androidx.compose.ui.viewinterop.AndroidView
|
||||
private const val LOGIN_URL = "https://lobste.rs/login"
|
||||
private const val LOBSTERS_URL = "https://lobste.rs"
|
||||
|
||||
internal fun parseAuthenticatedUsername(usernameResult: String): String? {
|
||||
return usernameResult.removeSurrounding("\"").takeUnless { it.isBlank() || it == "null" }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: (String) -> Unit,
|
||||
onLoginSuccess: (String, String) -> Unit,
|
||||
popBackStack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -72,6 +77,7 @@ fun LoginScreen(
|
||||
factory = { context ->
|
||||
WebView(context)
|
||||
.apply {
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
settings.javaScriptEnabled = true
|
||||
webViewClient =
|
||||
object : WebViewClient() {
|
||||
@@ -81,15 +87,20 @@ fun LoginScreen(
|
||||
|
||||
override fun onPageFinished(view: WebView, url: String) {
|
||||
isLoading = false
|
||||
// Both /login and /login/2fa contain "/login" — only act when
|
||||
// navigating away from both (i.e., successful authentication).
|
||||
// CookieManager requires the full URL scheme+host to return cookies.
|
||||
if (!url.contains("/login")) {
|
||||
val cookie = CookieManager.getInstance().getCookie(LOBSTERS_URL)
|
||||
if (cookie != null) {
|
||||
onLoginSuccess(cookie)
|
||||
@SuppressLint("DeprecatedCall")
|
||||
view.evaluateJavascript(
|
||||
"document.body && document.body.getAttribute('data-username')"
|
||||
) { usernameResult ->
|
||||
val username = parseAuthenticatedUsername(usernameResult)
|
||||
if (username != null) {
|
||||
val cookie = CookieManager.getInstance().getCookie(LOBSTERS_URL)
|
||||
if (cookie != null) {
|
||||
onLoginSuccess(cookie, username)
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,14 @@ import kotlinx.coroutines.launch
|
||||
private const val JSON_MIME_TYPE = "application/json"
|
||||
private const val HTML_MIME_TYPE = "application/html"
|
||||
|
||||
internal fun loggedInAccountText(username: String?): String {
|
||||
return if (username.isNullOrBlank()) {
|
||||
"Sign out of your lobste.rs account"
|
||||
} else {
|
||||
"Logged in as $username"
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
openLibrariesScreen: () -> Unit,
|
||||
@@ -63,6 +71,7 @@ fun SettingsScreen(
|
||||
openLoginScreen: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
isLoggedIn: Boolean,
|
||||
username: String?,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
openInputStream: (Uri) -> InputStream?,
|
||||
openOutputStream: (Uri) -> OutputStream?,
|
||||
@@ -80,7 +89,7 @@ fun SettingsScreen(
|
||||
if (isLoggedIn) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Log out") },
|
||||
supportingContent = { Text("Sign out of your lobste.rs account") },
|
||||
supportingContent = { Text(loggedInAccountText(username)) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.AccountCircle,
|
||||
@@ -381,6 +390,7 @@ private fun SettingsScreenPreview() {
|
||||
openLoginScreen = {},
|
||||
onLogout = {},
|
||||
isLoggedIn = false,
|
||||
username = null,
|
||||
snackbarHostState = SnackbarHostState(),
|
||||
openInputStream = { null },
|
||||
openOutputStream = { null },
|
||||
|
||||
@@ -20,7 +20,10 @@ import kotlinx.coroutines.flow.stateIn
|
||||
@Inject
|
||||
@ViewModelKey
|
||||
@ContributesIntoMap(scope = AppScope::class, binding = binding<ViewModel>())
|
||||
class SettingsViewModel(private val sessionCookieStore: SessionCookieStore) : ViewModel() {
|
||||
class SettingsViewModel(
|
||||
private val sessionCookieStore: SessionCookieStore,
|
||||
private val webViewCookieStore: WebViewCookieStore,
|
||||
) : ViewModel() {
|
||||
|
||||
val isLoggedIn =
|
||||
sessionCookieStore
|
||||
@@ -31,11 +34,21 @@ class SettingsViewModel(private val sessionCookieStore: SessionCookieStore) : Vi
|
||||
initialValue = false,
|
||||
)
|
||||
|
||||
val username =
|
||||
sessionCookieStore
|
||||
.username()
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = null,
|
||||
)
|
||||
|
||||
fun logout() {
|
||||
sessionCookieStore.clear()
|
||||
webViewCookieStore.clearLobstersCookies()
|
||||
}
|
||||
|
||||
fun saveCookie(cookie: String) {
|
||||
sessionCookieStore.set(cookie)
|
||||
fun saveCookie(cookie: String, username: String) {
|
||||
sessionCookieStore.set(cookie, username)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import android.webkit.CookieManager
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import dev.zacsweers.metro.Inject
|
||||
|
||||
interface WebViewCookieStore {
|
||||
fun clearLobstersCookies()
|
||||
}
|
||||
|
||||
@Inject
|
||||
@ContributesBinding(AppScope::class)
|
||||
class AndroidWebViewCookieStore : WebViewCookieStore {
|
||||
override fun clearLobstersCookies() {
|
||||
CookieManager.getInstance().removeAllCookies(null)
|
||||
CookieManager.getInstance().flush()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.android.viewmodel
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import dev.msfjarvis.claw.core.network.SessionCookieStore
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SettingsViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `logout clears persisted and webview sessions`() {
|
||||
val sessionCookieStore = FakeSessionCookieStore()
|
||||
val webViewCookieStore = FakeWebViewCookieStore()
|
||||
val viewModel = SettingsViewModel(sessionCookieStore, webViewCookieStore)
|
||||
|
||||
viewModel.logout()
|
||||
|
||||
assertThat(sessionCookieStore.cleared).isTrue()
|
||||
assertThat(webViewCookieStore.cleared).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `saveCookie persists username`() {
|
||||
val sessionCookieStore = FakeSessionCookieStore()
|
||||
val viewModel = SettingsViewModel(sessionCookieStore, FakeWebViewCookieStore())
|
||||
|
||||
viewModel.saveCookie("cookie=value", "alice")
|
||||
|
||||
assertThat(sessionCookieStore.savedCookie).isEqualTo("cookie=value")
|
||||
assertThat(sessionCookieStore.savedUsername).isEqualTo("alice")
|
||||
}
|
||||
|
||||
private class FakeSessionCookieStore : SessionCookieStore {
|
||||
var cleared = false
|
||||
var savedCookie: String? = null
|
||||
var savedUsername: String? = null
|
||||
|
||||
override fun get(): String? = savedCookie
|
||||
|
||||
override fun getUsername(): String? = savedUsername
|
||||
|
||||
override fun set(cookie: String, username: String) {
|
||||
savedCookie = cookie
|
||||
savedUsername = username
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
cleared = true
|
||||
savedCookie = null
|
||||
savedUsername = null
|
||||
}
|
||||
|
||||
override fun isLoggedIn(): Flow<Boolean> = flowOf(savedUsername != null)
|
||||
|
||||
override fun username(): Flow<String?> = flowOf(savedUsername)
|
||||
}
|
||||
|
||||
private class FakeWebViewCookieStore : WebViewCookieStore {
|
||||
var cleared = false
|
||||
|
||||
override fun clearLobstersCookies() {
|
||||
cleared = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import dev.burnoo.kspoon.Kspoon
|
||||
import dev.msfjarvis.claw.model.LobstersPostDetails
|
||||
import dev.msfjarvis.claw.model.User
|
||||
import dev.msfjarvis.claw.util.TestUtils.assertIs
|
||||
import java.time.Instant
|
||||
import java.time.format.DateTimeFormatter
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -135,6 +136,16 @@ class ApiTest {
|
||||
assertThat(postDetails.comments.single().score).isEqualTo(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `edited comments expose a single timestamp and edited state`() = runTest {
|
||||
val postDetails = api.getPostDetails("tdfoqh")
|
||||
assertIs<Success<LobstersPostDetails>>(postDetails)
|
||||
|
||||
val editedComment = postDetails.value.comments.first { it.shortId == "pcvbcd" }
|
||||
assertThat(editedComment.edited).isTrue()
|
||||
assertThat(Instant.from(editedComment.timestamp).epochSecond).isEqualTo(1658588955)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `post details preserves upvoted comments`() = runTest {
|
||||
val postDetails = wrapper.upvotedPostDetails
|
||||
|
||||
@@ -273,8 +273,8 @@ private fun CommentEntry(
|
||||
buildCommenterString(
|
||||
commenterName = comment.user,
|
||||
score = comment.score,
|
||||
createdAt = comment.createdAt,
|
||||
lastEditedAt = comment.lastEditedAt,
|
||||
timestamp = comment.timestamp,
|
||||
edited = comment.edited,
|
||||
nameColorOverride =
|
||||
if (commentNode.isPostAuthor) MaterialTheme.colorScheme.tertiary else null,
|
||||
),
|
||||
@@ -293,20 +293,14 @@ private fun CommentEntry(
|
||||
private fun buildCommenterString(
|
||||
commenterName: String,
|
||||
score: Int,
|
||||
createdAt: TemporalAccessor,
|
||||
lastEditedAt: TemporalAccessor,
|
||||
timestamp: TemporalAccessor,
|
||||
edited: Boolean,
|
||||
nameColorOverride: Color? = null,
|
||||
): AnnotatedString {
|
||||
val now = System.currentTimeMillis()
|
||||
val createdRelative =
|
||||
val relativeTime =
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
Instant.from(createdAt).toEpochMilli(),
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
)
|
||||
val lastEditedRelative =
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
Instant.from(lastEditedAt).toEpochMilli(),
|
||||
Instant.from(timestamp).toEpochMilli(),
|
||||
now,
|
||||
DateUtils.MINUTE_IN_MILLIS,
|
||||
)
|
||||
@@ -323,13 +317,11 @@ private fun buildCommenterString(
|
||||
append(' ')
|
||||
append('•')
|
||||
append(' ')
|
||||
append(createdRelative.toString())
|
||||
if (lastEditedRelative != createdRelative) {
|
||||
append(relativeTime.toString())
|
||||
if (edited) {
|
||||
append(' ')
|
||||
append('(')
|
||||
append("Edited")
|
||||
append(' ')
|
||||
append(lastEditedRelative.toString())
|
||||
append(')')
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -31,8 +31,12 @@ class SqlDelightSessionCookieStore(
|
||||
return queries.get().executeAsOneOrNull()
|
||||
}
|
||||
|
||||
override fun set(cookie: String) {
|
||||
queries.upsert(cookie)
|
||||
override fun getUsername(): String? {
|
||||
return queries.getUsername().executeAsOneOrNull()?.username
|
||||
}
|
||||
|
||||
override fun set(cookie: String, username: String) {
|
||||
queries.upsert(cookie, username)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
@@ -40,6 +44,12 @@ class SqlDelightSessionCookieStore(
|
||||
}
|
||||
|
||||
override fun isLoggedIn(): Flow<Boolean> {
|
||||
return queries.get().asFlow().mapToOneOrNull(readDispatcher).map { it != null }
|
||||
return queries.getUsername().asFlow().mapToOneOrNull(readDispatcher).map {
|
||||
it?.username != null
|
||||
}
|
||||
}
|
||||
|
||||
override fun username(): Flow<String?> {
|
||||
return queries.getUsername().asFlow().mapToOneOrNull(readDispatcher).map { it?.username }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ class CommentsHandlerTest {
|
||||
comment = "Comment $shortId",
|
||||
url = "https://lobste.rs/s/$shortId",
|
||||
score = 1,
|
||||
createdAt = Instant.EPOCH,
|
||||
lastEditedAt = Instant.EPOCH,
|
||||
timestamp = Instant.EPOCH,
|
||||
edited = false,
|
||||
parentComment = parentComment,
|
||||
user = "user-$shortId",
|
||||
)
|
||||
|
||||
@@ -35,6 +35,6 @@ class LobstersCookieJar(private val store: SessionCookieStore) : CookieJar {
|
||||
// Serialize back to a single Set-Cookie header value using the first matching cookie.
|
||||
// lobste.rs sets one session cookie (lobsters_trap); joining handles edge cases.
|
||||
val raw = cookies.joinToString("; ") { "${it.name}=${it.value}" }
|
||||
store.set(raw)
|
||||
store.set(raw, store.getUsername().orEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,13 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface SessionCookieStore {
|
||||
fun get(): String?
|
||||
|
||||
fun set(cookie: String)
|
||||
fun getUsername(): String?
|
||||
|
||||
fun set(cookie: String, username: String)
|
||||
|
||||
fun clear()
|
||||
|
||||
fun isLoggedIn(): Flow<Boolean>
|
||||
|
||||
fun username(): Flow<String?>
|
||||
}
|
||||
|
||||
+6
-2
@@ -1,13 +1,17 @@
|
||||
CREATE TABLE SessionCookie (
|
||||
id INTEGER NOT NULL PRIMARY KEY DEFAULT 1,
|
||||
value TEXT NOT NULL
|
||||
value TEXT NOT NULL,
|
||||
username TEXT
|
||||
);
|
||||
|
||||
get:
|
||||
SELECT value FROM SessionCookie WHERE id = 1;
|
||||
|
||||
getUsername:
|
||||
SELECT username FROM SessionCookie WHERE id = 1;
|
||||
|
||||
upsert:
|
||||
INSERT OR REPLACE INTO SessionCookie(id, value) VALUES (1, ?);
|
||||
INSERT OR REPLACE INTO SessionCookie(id, value, username) VALUES (1, ?, ?);
|
||||
|
||||
deleteAll:
|
||||
DELETE FROM SessionCookie;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE SessionCookie ADD COLUMN username TEXT;
|
||||
@@ -36,10 +36,10 @@ class Comment(
|
||||
val score: Int = 1,
|
||||
@Serializable(with = CommentInstantSerializer::class)
|
||||
@Selector("div.byline a[href^=/c/] time", attr = "data-at-unix", defValue = "")
|
||||
val createdAt: TemporalAccessor,
|
||||
@Serializable(with = CommentInstantSerializer::class)
|
||||
@Selector("div.byline a[href^=/c/] time", attr = "data-at-unix", defValue = "")
|
||||
val lastEditedAt: TemporalAccessor,
|
||||
val timestamp: TemporalAccessor,
|
||||
@Serializable(with = CommentEditedSerializer::class)
|
||||
@Selector("div.byline span", defValue = "")
|
||||
val edited: Boolean = false,
|
||||
@Serializable(with = EmptyStringAsNullSerializer::class)
|
||||
@Selector(":root", attr = "data-parent-shortid", defValue = "")
|
||||
val parentComment: String? = null,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright © Harsh Shandilya.
|
||||
* Use of this source code is governed by an MIT-style
|
||||
* license that can be found in the LICENSE file or at
|
||||
* https://opensource.org/licenses/MIT.
|
||||
*/
|
||||
package dev.msfjarvis.claw.model
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
|
||||
internal object CommentEditedSerializer : KSerializer<Boolean> {
|
||||
override val descriptor: SerialDescriptor =
|
||||
PrimitiveSerialDescriptor("CommentEdited", PrimitiveKind.BOOLEAN)
|
||||
|
||||
override fun deserialize(decoder: Decoder): Boolean = decoder.decodeString().contains("edited")
|
||||
|
||||
override fun serialize(encoder: Encoder, value: Boolean) = encoder.encodeBoolean(value)
|
||||
}
|
||||
@@ -53,8 +53,10 @@ internal object CommentsSerializer : KSerializer<List<Comment>> {
|
||||
}
|
||||
|
||||
private fun Element.toComment(parentComment: String?): Comment {
|
||||
val timestamp = selectFirst("div.byline a[href^=/c/] time")?.attr("data-at-unix").orEmpty()
|
||||
val byline = selectFirst("div.byline")
|
||||
val timestamp = byline?.selectFirst("a[href^=/c/] time")?.attr("data-at-unix").orEmpty()
|
||||
val parsedTimestamp = timestamp.toTemporalAccessor()
|
||||
val isEdited = byline?.text()?.contains("edited") == true
|
||||
return Comment(
|
||||
shortId = attr("data-shortid"),
|
||||
comment = selectFirst("div.comment_text")?.html().orEmpty(),
|
||||
@@ -68,8 +70,8 @@ internal object CommentsSerializer : KSerializer<List<Comment>> {
|
||||
?.trim()
|
||||
?.takeUnless { it == "~" }
|
||||
?.toIntOrNull() ?: 1,
|
||||
createdAt = parsedTimestamp,
|
||||
lastEditedAt = parsedTimestamp,
|
||||
timestamp = parsedTimestamp,
|
||||
edited = isEdited,
|
||||
parentComment = parentComment,
|
||||
user =
|
||||
getElementsByClass("byline")
|
||||
|
||||
Reference in New Issue
Block a user