[1.495.*] Pre-release merge (#1095)

This commit is contained in:
tramline-github[bot]
2026-03-09 05:50:26 +00:00
committed by GitHub
11 changed files with 100 additions and 81 deletions
@@ -56,7 +56,14 @@ fun DatabasePosts(
items.forEach { (month, posts) ->
stickyHeader(contentType = "month-header") { MonthHeader(label = month) }
items(items = posts, key = { it.shortId }, contentType = { "LobstersItem" }) { item ->
LobstersListItem(item = item, postActions = postActions)
val isSaved = postActions.isPostSaved(item)
val isRead = postActions.isPostRead(item)
LobstersListItem(
item = item,
isSaved = isSaved,
isRead = isRead,
postActions = postActions,
)
HorizontalDivider()
}
}
@@ -25,7 +25,13 @@ import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox
@Composable
fun LobstersListItem(item: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
fun LobstersListItem(
item: UIPost,
isSaved: Boolean,
isRead: Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
val commentsAction =
SwipeAction(
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
@@ -44,12 +50,25 @@ fun LobstersListItem(item: UIPost, postActions: PostActions, modifier: Modifier
swipeThreshold = 80.dp,
backgroundUntilSwipeThreshold = MaterialTheme.colorScheme.surfaceVariant,
) {
LobstersCard(post = item, postActions = postActions, modifier = modifier)
LobstersCard(
post = item,
isSaved = isSaved,
isRead = isRead,
postActions = postActions,
modifier = modifier,
)
}
}
@ThemePreviews
@Composable
private fun ItemPreview() {
LobstersTheme { LobstersListItem(item = samplePosts(1).first(), postActions = TEST_POST_ACTIONS) }
LobstersTheme {
LobstersListItem(
item = samplePosts(1).first(),
isSaved = true,
isRead = true,
postActions = TEST_POST_ACTIONS,
)
}
}
@@ -87,7 +87,14 @@ fun NetworkPosts(
if (item != null) {
val shouldShowPost = item.tags.none { tag -> filteredTags.contains(tag) }
if (shouldShowPost) {
LobstersListItem(item = item, postActions = postActions)
val isSaved = postActions.isPostSaved(item)
val isRead = postActions.isPostRead(item)
LobstersListItem(
item = item,
isSaved = isSaved,
isRead = isRead,
postActions = postActions,
)
HorizontalDivider()
}
}
@@ -119,12 +119,16 @@ class ClawViewModel(
var searchQuery by mutableStateOf("")
private var _readPosts = emptyList<String>()
private var _savedPosts = emptyList<String>()
private var _readPosts by mutableStateOf(emptySet<String>())
private var _savedPosts by mutableStateOf(emptySet<String>())
init {
viewModelScope.launch { savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId) } }
viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } }
viewModelScope.launch {
savedPosts.collectLatest { _savedPosts = it.map(UIPost::shortId).toSet() }
}
viewModelScope.launch {
readPostsRepository.readPosts.collectLatest { _readPosts = it.toSet() }
}
}
fun toggleSave(post: UIPost) {
@@ -18,9 +18,11 @@ import retrofit2.Retrofit
object SearchConverter : Converter<ResponseBody, List<LobstersPost>> {
override fun convert(value: ResponseBody): List<LobstersPost> {
val elements =
Jsoup.parse(value.string(), LobstersApi.BASE_URL).select("div.story_liner.h-entry")
return elements.map(::parsePost)
return value.byteStream().use { stream ->
Jsoup.parse(stream, "UTF-8", LobstersApi.BASE_URL)
.select("div.story_liner.h-entry")
.map(::parsePost)
}
}
private fun parsePost(elem: Element): LobstersPost {
@@ -50,9 +50,8 @@ class BaselineProfileBenchmark {
compilationMode = compilationMode,
startupMode = StartupMode.COLD,
iterations = 10,
setupBlock = { device.executeShellCommand("pm clear $PACKAGE_NAME") },
) {
device.executeShellCommand("pm clear $PACKAGE_NAME")
startActivityAndWait()
device.waitForIdle()
@@ -27,7 +27,7 @@ class KotlinCommonPlugin : Plugin<Project> {
}
}
withType<Test>().configureEach {
maxParallelForks = Runtime.getRuntime().availableProcessors() * 2
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
useJUnitPlatform()
}
@@ -14,17 +14,30 @@ import kotlinx.coroutines.flow.asStateFlow
internal class CommentsHandler {
private val _listItems: MutableStateFlow<List<CommentNode>> = MutableStateFlow(emptyList())
val listItems: StateFlow<List<CommentNode>> = _listItems.asStateFlow()
private var rootNodes = emptyList<CommentNode>()
private val _visibleNodes: MutableStateFlow<List<CommentNode>> = MutableStateFlow(emptyList())
val listItems: StateFlow<List<CommentNode>> = _visibleNodes.asStateFlow()
private var collapsedCommentIds = setOf<String>()
private fun updateVisibleNodes() {
val flatList = mutableListOf<CommentNode>()
fun traverse(node: CommentNode) {
flatList.add(node)
if (node.isExpanded) {
node.children.forEach { traverse(it) }
}
}
rootNodes.forEach { traverse(it) }
_visibleNodes.value = flatList
}
fun createListNode(
comments: List<Comment>,
commentState: PostComments?,
isPostAuthor: (Comment) -> Boolean,
) {
val commentNodes = mutableListOf<CommentNode>()
val isUnread = { id: String -> commentState?.commentIds?.contains(id) == false }
val isUnread = { id: String -> commentState?.commentIds?.contains(id) != true }
for (i in comments.indices) {
val comment = comments[i]
@@ -53,7 +66,8 @@ internal class CommentsHandler {
}
}
_listItems.value = commentNodes
rootNodes = commentNodes
updateVisibleNodes()
}
fun updateListNode(shortId: String, isExpanded: Boolean) {
@@ -65,9 +79,9 @@ internal class CommentsHandler {
return node.copy(children = updatedChildren)
}
val listNode = _listItems.value.map { updateNode(it) }
_listItems.value = listNode
rootNodes = rootNodes.map { updateNode(it) }
syncCollapsedState()
updateVisibleNodes()
}
private fun syncCollapsedState() {
@@ -79,7 +93,7 @@ internal class CommentsHandler {
}
}
}
collapsedCommentIds = collectCollapsedIds(_listItems.value)
collapsedCommentIds = collectCollapsedIds(rootNodes)
}
fun updateUnreadStatus(commentState: PostComments?) {
@@ -91,7 +105,7 @@ internal class CommentsHandler {
return node.copy(isUnread = isUnread, children = updatedChildren)
}
val listNode = _listItems.value.map { updateNode(it) }
_listItems.value = listNode
rootNodes = rootNodes.map { updateNode(it) }
updateVisibleNodes()
}
}
@@ -8,12 +8,6 @@ package dev.msfjarvis.claw.common.comments
import android.text.format.DateUtils
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -66,8 +60,6 @@ import java.time.Instant
import java.time.temporal.TemporalAccessor
import kotlinx.coroutines.flow.StateFlow
private const val AnimationDuration = 100
@Composable
internal fun CommentsPageInternal(
details: UIPost,
@@ -130,7 +122,7 @@ internal fun CommentsPageInternal(
}
items(items = commentNodes, key = { node -> node.comment.shortId }) { node ->
Node(node, openUserProfile, onToggleExpandedState)
NodeBox(node, node.isExpanded, openUserProfile, onToggleExpandedState)
}
item(key = "bottom_spacer") {
@@ -151,38 +143,6 @@ internal fun CommentsPageInternal(
}
}
/**
* Simple tree view implementation by Anton Shilov who was smarter in 2020 than I am today
* https://gist.github.com/antonshilov/ef8cd0a360a5cc0f823b2a4e85084720
*/
@Composable
private fun Node(
node: CommentNode,
openUserProfile: (String) -> Unit,
onToggleExpandedState: (String, Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
NodeBox(
node = node,
isExpanded = node.isExpanded,
openUserProfile = openUserProfile,
onToggleExpandedState = onToggleExpandedState,
modifier = modifier,
)
AnimatedVisibility(
visible = node.isExpanded,
enter = fadeIn(tween(AnimationDuration)) + expandVertically(tween(AnimationDuration)),
exit = fadeOut(tween(AnimationDuration)) + shrinkVertically(tween(AnimationDuration)),
) {
Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
node.children.forEach { model -> Node(model, openUserProfile, onToggleExpandedState) }
}
}
}
}
@Composable
private fun NodeBox(
node: CommentNode,
@@ -40,8 +40,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -61,9 +59,13 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun LobstersCard(post: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
val readState by remember(post.shortId) { mutableStateOf(postActions.isPostRead(post)) }
var savedState by remember(post.shortId) { mutableStateOf(postActions.isPostSaved(post)) }
fun LobstersCard(
post: UIPost,
isSaved: Boolean,
isRead: Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
Box(
modifier =
modifier
@@ -78,7 +80,7 @@ fun LobstersCard(post: UIPost, postActions: PostActions, modifier: Modifier = Mo
) {
PostDetails(
post = post,
isRead = { readState },
isRead = { isRead },
singleLineTitle = true,
modifier = Modifier.weight(1f),
)
@@ -87,15 +89,9 @@ fun LobstersCard(post: UIPost, postActions: PostActions, modifier: Modifier = Mo
horizontalAlignment = Alignment.CenterHorizontally,
) {
SaveButton(
isSaved = savedState,
isSaved = isSaved,
modifier =
Modifier.clickable(
role = Role.Button,
onClick = {
postActions.toggleSave(post)
savedState = !savedState
},
),
Modifier.clickable(role = Role.Button, onClick = { postActions.toggleSave(post) }),
)
HorizontalDivider(modifier = Modifier.width(48.dp))
CommentsButton(
@@ -295,5 +291,7 @@ val TEST_POST =
@ThemePreviews
@Composable
private fun LobstersCardPreview() {
LobstersTheme { LobstersCard(post = TEST_POST, postActions = TEST_POST_ACTIONS) }
LobstersTheme {
LobstersCard(post = TEST_POST, isSaved = true, isRead = true, postActions = TEST_POST_ACTIONS)
}
}
@@ -7,6 +7,8 @@
package dev.msfjarvis.claw.common.theme
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
@@ -103,7 +105,7 @@ fun LobstersTheme(
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
val window = view.context.findActivity()?.window ?: return@SideEffect
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
@@ -111,3 +113,10 @@ fun LobstersTheme(
MaterialTheme(colorScheme = colorScheme, typography = AppTypography) { content() }
}
}
internal tailrec fun Context.findActivity(): Activity? =
when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}