refactor(android): switch over to PagingData transformations

This commit is contained in:
Harsh Shandilya 2024-01-27 16:52:47 +05:30
parent 051b7ab2bb
commit 74a7835a53
19 changed files with 187 additions and 151 deletions

View file

@ -28,11 +28,11 @@ import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import dev.msfjarvis.claw.common.theme.DarkThemeColors
import dev.msfjarvis.claw.common.theme.LightThemeColors
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
class SavedPostsWidget(private val posts: List<UIPost>) : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceTheme(
@ -46,7 +46,7 @@ class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
}
@Composable
fun WidgetHost(posts: ImmutableList<SavedPost>, modifier: GlanceModifier = GlanceModifier) {
fun WidgetHost(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
LazyColumn(
modifier =
modifier.fillMaxSize().background(GlanceTheme.colors.background).appWidgetBackground(),

View file

@ -33,13 +33,13 @@ import androidx.glance.text.TextStyle
import dev.msfjarvis.claw.android.MainActivity
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
import dev.msfjarvis.claw.android.R
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
private val destinationKey = Key<String>(NAVIGATION_KEY)
@Composable
@GlanceComposable
fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier) {
fun WidgetListEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
val titleStyle = MaterialTheme.typography.titleMedium
val commentsAction =
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
@ -68,7 +68,7 @@ fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier)
)
Image(
provider = ImageProvider(R.drawable.ic_comment),
contentDescription = "${post.commentCount ?: 0} comments",
contentDescription = "${post.commentCount} comments",
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction),
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2022-2023 Harsh Shandilya.
* Copyright © 2022-2024 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.
@ -16,9 +16,8 @@ import dev.msfjarvis.claw.android.ui.navigation.Destinations
import dev.msfjarvis.claw.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
fun Context.getActivity(): ComponentActivity? {
return when (this) {
@ -59,11 +58,11 @@ fun rememberPostActions(
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
}
override fun toggleSave(post: SavedPost) {
override fun toggleSave(post: UIPost) {
viewModel.toggleSave(post)
}
override suspend fun getComments(postId: String): LobstersPostDetails {
override suspend fun getComments(postId: String): UIPost {
return viewModel.getPostComments(postId)
}

View file

@ -27,13 +27,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.decorations.MonthHeader
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
import kotlinx.collections.immutable.ImmutableMap
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DatabasePosts(
items: ImmutableMap<String, List<SavedPost>>,
items: ImmutableMap<String, List<UIPost>>,
listState: LazyListState,
postActions: PostActions,
modifier: Modifier = Modifier,
@ -54,12 +54,7 @@ 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,
isSaved = { true },
isRead = { false },
postActions = postActions,
)
LobstersListItem(item = item, postActions = postActions)
HorizontalDivider()
}
}

View file

@ -11,24 +11,16 @@ import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import dev.msfjarvis.claw.common.posts.LobstersCard
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.UIPost
import me.saket.swipe.SwipeAction
import me.saket.swipe.SwipeableActionsBox
@Composable
fun LobstersListItem(
item: SavedPost,
isSaved: (SavedPost) -> Boolean,
isRead: suspend (String) -> Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
val read by produceState(false, item.shortId) { value = isRead(item.shortId) }
fun LobstersListItem(item: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
val commentsAction =
SwipeAction(
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
@ -36,12 +28,6 @@ fun LobstersListItem(
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
)
SwipeableActionsBox(endActions = listOf(commentsAction)) {
LobstersCard(
post = item,
isSaved = isSaved(item),
isRead = read,
postActions = postActions,
modifier = modifier,
)
LobstersCard(post = item, postActions = postActions, modifier = modifier)
}
}

View file

@ -27,9 +27,7 @@ import androidx.paging.compose.itemKey
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.NetworkError
import dev.msfjarvis.claw.common.ui.ProgressBar
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.toSavedPost
import dev.msfjarvis.claw.model.UIPost
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
import eu.bambooapps.material3.pullrefresh.pullRefresh
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
@ -37,10 +35,8 @@ import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NetworkPosts(
lazyPagingItems: LazyPagingItems<LobstersPost>,
lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState,
isPostSaved: (SavedPost) -> Boolean,
isPostRead: suspend (String) -> Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
@ -64,13 +60,7 @@ fun NetworkPosts(
) { index ->
val item = lazyPagingItems[index]
if (item != null) {
val dbModel = item.toSavedPost()
LobstersListItem(
item = dbModel,
isSaved = isPostSaved,
isRead = isPostRead,
postActions = postActions,
)
LobstersListItem(item = item, postActions = postActions)
HorizontalDivider()
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2023 Harsh Shandilya.
* Copyright © 2023-2024 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.
@ -21,15 +21,13 @@ import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.SearchBar
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.UIPost
import kotlinx.coroutines.flow.Flow
@Composable
fun SearchList(
items: Flow<PagingData<LobstersPost>>,
items: Flow<PagingData<UIPost>>,
listState: LazyListState,
isPostSaved: (SavedPost) -> Boolean,
postActions: PostActions,
searchQuery: String,
setSearchQuery: (String) -> Unit,
@ -50,8 +48,6 @@ fun SearchList(
NetworkPosts(
lazyPagingItems = lazyPagingItems,
listState = listState,
isPostSaved = isPostSaved,
isPostRead = { false },
postActions = postActions,
)
}

View file

@ -228,8 +228,6 @@ fun LobstersPostsScreen(
NetworkPosts(
lazyPagingItems = hottestPosts,
listState = hottestListState,
isPostSaved = viewModel::isPostSaved,
isPostRead = viewModel::isPostRead,
postActions = postActions,
)
}
@ -238,8 +236,6 @@ fun LobstersPostsScreen(
NetworkPosts(
lazyPagingItems = newestPosts,
listState = newestListState,
isPostSaved = viewModel::isPostSaved,
isPostRead = viewModel::isPostRead,
postActions = postActions,
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2023 Harsh Shandilya.
* Copyright © 2023-2024 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.
@ -47,7 +47,6 @@ fun SearchScreen(
SearchList(
items = viewModel.searchResults,
listState = listState,
isPostSaved = viewModel::isPostSaved,
postActions = postActions,
searchQuery = viewModel.searchQuery,
setSearchQuery = { query -> viewModel.searchQuery = query },

View file

@ -18,6 +18,9 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.map
import com.deliveryhero.whetstone.app.ApplicationScope
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
import com.slack.eithernet.ApiResult.Failure
@ -31,8 +34,12 @@ import dev.msfjarvis.claw.android.paging.SearchPagingSource
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.core.injection.MainDispatcher
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.Comment
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.fromSavedPost
import dev.msfjarvis.claw.model.toSavedPost
import dev.msfjarvis.claw.model.toUIPost
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@ -44,9 +51,11 @@ import javax.inject.Inject
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@ -68,34 +77,43 @@ constructor(
@MainDispatcher private val mainDispatcher: CoroutineDispatcher,
@ForScope(ApplicationScope::class) context: Context,
) : AndroidViewModel(context as Application) {
private val hottestPostsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
pagingSourceFactory.create(api::getHottestPosts)
}
private val newestPostsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
pagingSourceFactory.create(api::getNewestPosts)
}
private val searchResultsPager =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
searchPagingSourceFactory.create { searchQuery }
}
val hottestPosts =
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
)
.flow
.map(::mapUIPost)
.cachedIn(viewModelScope)
val hottestPosts
get() = hottestPostsPager.flow
val newestPosts
get() = newestPostsPager.flow
val newestPosts =
Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) },
)
.flow
.map(::mapUIPost)
.cachedIn(viewModelScope)
val searchResults =
Pager(
PagingConfig(pageSize = PAGE_SIZE),
initialKey = STARTING_PAGE_INDEX,
pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } },
)
.flow
.map(::mapUIPost)
val savedPosts
get() = savedPostsRepository.savedPosts
get() =
savedPostsRepository.savedPosts
.map { it.map(UIPost.Companion::fromSavedPost) }
.shareIn(viewModelScope, started = SharingStarted.Lazily, Int.MAX_VALUE)
val savedPostsByMonth
get() = savedPosts.map(::mapSavedPosts)
val searchResults
get() = searchResultsPager.flow
var searchQuery by mutableStateOf("")
private val _savedPostsMutex = Mutex()
@ -104,12 +122,19 @@ constructor(
init {
viewModelScope.launch {
savedPosts.collectLatest {
_savedPostsMutex.withLock { _savedPosts = it.map(SavedPost::shortId) }
_savedPostsMutex.withLock { _savedPosts = it.map(UIPost::shortId) }
}
}
}
private fun mapSavedPosts(items: List<SavedPost>): ImmutableMap<String, List<SavedPost>> {
private fun mapUIPost(pagingData: PagingData<LobstersPost>): PagingData<UIPost> {
return pagingData.map { post ->
val uiPost = post.toUIPost()
uiPost.copy(isSaved = isPostSaved(uiPost), isRead = isPostRead(uiPost))
}
}
private fun mapSavedPosts(items: List<UIPost>): ImmutableMap<String, List<UIPost>> {
val sorted =
items.sortedWith { post1, post2 ->
val post1Date = post1.createdAt.toLocalDateTime()
@ -130,17 +155,19 @@ constructor(
.toImmutableMap()
}
fun isPostSaved(post: SavedPost): Boolean {
private fun isPostSaved(post: UIPost): Boolean {
return _savedPosts.contains(post.shortId)
}
fun toggleSave(post: SavedPost) {
private fun isPostRead(post: UIPost) = readPostsRepository.isRead(post.shortId)
fun toggleSave(post: UIPost) {
viewModelScope.launch(ioDispatcher) {
val saved = isPostSaved(post)
if (saved) {
savedPostsRepository.removePost(post)
savedPostsRepository.removePost(post.toSavedPost())
} else {
savedPostsRepository.savePost(post)
savedPostsRepository.savePost(post.toSavedPost())
}
val newPosts = savedPosts.first()
withContext(mainDispatcher) {
@ -152,7 +179,7 @@ constructor(
suspend fun getPostComments(postId: String) =
withContext(ioDispatcher) {
when (val result = api.getPostDetails(postId)) {
is Success -> result.value
is Success -> result.value.toUIPost()
is Failure.NetworkFailure -> throw result.error
is Failure.UnknownFailure -> throw result.error
is Failure.HttpFailure -> {
@ -198,8 +225,6 @@ constructor(
viewModelScope.launch { readPostsRepository.markRead(postId) }
}
suspend fun isPostRead(postId: String) = readPostsRepository.isRead(postId)
/**
* Parses a given [String] into a [LocalDateTime]. This method is only intended to be used for
* dates in the format returned by the Lobsters API, and is not a general purpose parsing

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2021-2023 Harsh Shandilya.
* Copyright © 2021-2024 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.
@ -23,6 +23,7 @@ constructor(
withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
}
suspend fun isRead(postId: String): Boolean =
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null }
fun isRead(postId: String): Boolean {
return readPostsQueries.isRead(postId).executeAsOneOrNull() != null
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2022-2023 Harsh Shandilya.
* Copyright © 2022-2024 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.
@ -18,6 +18,8 @@ import dev.msfjarvis.claw.android.glance.SavedPostsWidget
import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.fromSavedPost
import dev.msfjarvis.claw.model.toSavedPost
import javax.inject.Inject
import kotlinx.coroutines.flow.first
@ -44,7 +46,10 @@ constructor(
.filterIsInstance<Success<LobstersPostDetails>>()
.map { result -> result.value.toSavedPost() }
.let { savedPostsRepository.savePosts(it) }
SavedPostsWidget(savedPostsRepository.savedPosts.first().take(50)).updateAll(applicationContext)
SavedPostsWidget(
savedPostsRepository.savedPosts.first().take(50).map(UIPost.Companion::fromSavedPost)
)
.updateAll(applicationContext)
return Result.success()
}
}

View file

@ -46,22 +46,22 @@ import dev.msfjarvis.claw.common.posts.TagRow
import dev.msfjarvis.claw.common.ui.NetworkImage
import dev.msfjarvis.claw.common.ui.ThemedRichText
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
import java.time.Instant
import java.time.temporal.TemporalAccessor
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun CommentsHeader(
postDetails: LobstersPostDetails,
post: UIPost,
postActions: PostActions,
htmlConverter: HTMLConverter,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current
val linkMetadata by
produceState(initialValue = LinkMetadata(postDetails.url, null)) {
runSuspendCatching { postActions.getLinkMetadata(postDetails.url) }
produceState(initialValue = LinkMetadata(post.url, null)) {
runSuspendCatching { postActions.getLinkMetadata(post.url) }
.onSuccess { metadata -> value = metadata }
}
@ -70,8 +70,8 @@ internal fun CommentsHeader(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
PostTitle(title = postDetails.title, isRead = false)
TagRow(tags = postDetails.tags.toImmutableList())
PostTitle(title = post.title, isRead = false)
TagRow(tags = post.tags.toImmutableList())
Spacer(Modifier.height(4.dp))
if (linkMetadata.url.isNotBlank()) {
@ -79,23 +79,23 @@ internal fun CommentsHeader(
linkMetadata = linkMetadata,
modifier =
Modifier.clickable {
postActions.viewPost(postDetails.shortId, linkMetadata.url, postDetails.commentsUrl)
postActions.viewPost(post.shortId, linkMetadata.url, post.commentsUrl)
},
)
Spacer(Modifier.height(4.dp))
}
if (postDetails.description.isNotBlank()) {
ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description))
if (post.description.isNotBlank()) {
ThemedRichText(htmlConverter.convertHTMLToMarkdown(post.description))
Spacer(Modifier.height(4.dp))
}
Submitter(
text = AnnotatedString("Submitted by ${postDetails.submitter.username}"),
avatarUrl = "https://lobste.rs/${postDetails.submitter.avatarUrl}",
contentDescription = "User avatar for ${postDetails.submitter.username}",
text = AnnotatedString("Submitted by ${post.submitter.username}"),
avatarUrl = "https://lobste.rs/${post.submitter.avatarUrl}",
contentDescription = "User avatar for ${post.submitter.username}",
modifier =
Modifier.clickable {
uriHandler.openUri("https://lobste.rs/u/${postDetails.submitter.username}")
uriHandler.openUri("https://lobste.rs/u/${post.submitter.username}")
},
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2021-2023 Harsh Shandilya.
* Copyright © 2021-2024 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.
@ -36,12 +36,12 @@ import dev.msfjarvis.claw.common.ui.NetworkError
import dev.msfjarvis.claw.common.ui.ProgressBar
import dev.msfjarvis.claw.database.local.PostComments
import dev.msfjarvis.claw.model.Comment
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
@Suppress("LongParameterList")
@Composable
private fun CommentsPageInternal(
details: LobstersPostDetails,
details: UIPost,
postActions: PostActions,
htmlConverter: HTMLConverter,
commentState: PostComments?,
@ -54,11 +54,7 @@ private fun CommentsPageInternal(
Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
item {
CommentsHeader(
postDetails = details,
postActions = postActions,
htmlConverter = htmlConverter,
)
CommentsHeader(post = details, postActions = postActions, htmlConverter = htmlConverter)
}
if (commentNodes.isNotEmpty()) {
@ -123,7 +119,7 @@ fun CommentsPage(
when (postDetails) {
is Success<*> -> {
CommentsPageInternal(
details = (postDetails as Success<LobstersPostDetails>).data,
details = (postDetails as Success<UIPost>).data,
postActions = postActions,
htmlConverter = htmlConverter,
commentState = commentState,

View file

@ -51,22 +51,15 @@ import androidx.compose.ui.unit.dp
import dev.msfjarvis.claw.common.theme.LobstersTheme
import dev.msfjarvis.claw.common.ui.NetworkImage
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.User
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun LobstersCard(
post: SavedPost,
isSaved: Boolean,
isRead: Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
var localSavedState by remember(post, isSaved) { mutableStateOf(isSaved) }
fun LobstersCard(post: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
var localSavedState by remember(post) { mutableStateOf(post.isSaved) }
Box(
modifier =
modifier
@ -79,7 +72,7 @@ fun LobstersCard(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
PostDetails(modifier = Modifier.weight(1f), post = post, isRead = isRead)
PostDetails(modifier = Modifier.weight(1f), post = post)
Column(
modifier = Modifier.wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(8.dp),
@ -108,15 +101,15 @@ fun LobstersCard(
}
@Composable
fun PostDetails(post: SavedPost, isRead: Boolean, modifier: Modifier = Modifier) {
fun PostDetails(post: UIPost, modifier: Modifier = Modifier) {
Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
PostTitle(title = post.title, isRead = isRead)
PostTitle(title = post.title, isRead = post.isRead)
TagRow(tags = post.tags.toImmutableList())
Spacer(Modifier.height(4.dp))
Submitter(
text = AnnotatedString("Submitted by ${post.submitterName}"),
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}",
contentDescription = "User avatar for ${post.submitterName}",
text = AnnotatedString("Submitted by ${post.submitter.username}"),
avatarUrl = "https://lobste.rs/${post.submitter.avatarUrl}",
contentDescription = "User avatar for ${post.submitter.username}",
)
}
}
@ -234,20 +227,19 @@ private fun LobstersCardPreview() {
LobstersTheme {
LobstersCard(
post =
SavedPost(
UIPost(
shortId = "ooga",
title = "Simple Anomaly Detection Using Plain SQL",
url = "https://hakibenita.com/sql-anomaly-detection",
createdAt = "2020-09-21T08:04:24.000-05:00",
commentCount = 1,
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
submitterName = "Haki",
submitterAvatarUrl = "/avatars/Haki-100.png",
submitter = User("Haki", "", "", "/avatars/Haki-100.png", ""),
tags = listOf("databases", "apis"),
description = "",
isSaved = true,
isRead = true,
),
isRead = true,
isSaved = true,
postActions =
object : PostActions {
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
@ -256,10 +248,10 @@ private fun LobstersCardPreview() {
override fun viewCommentsPage(commentsUrl: String) {}
override fun toggleSave(post: SavedPost) {}
override fun toggleSave(post: UIPost) {}
override suspend fun getComments(postId: String): LobstersPostDetails {
return LobstersPostDetails(
override suspend fun getComments(postId: String): UIPost {
return UIPost(
shortId = "ooga",
title = "Simple Anomaly Detection Using Plain SQL",
url = "https://hakibenita.com/sql-anomaly-detection",

View file

@ -1,5 +1,5 @@
/*
* Copyright © 2021-2023 Harsh Shandilya.
* Copyright © 2021-2024 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.
@ -7,9 +7,8 @@
package dev.msfjarvis.claw.common.posts
import androidx.compose.runtime.Stable
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails
import dev.msfjarvis.claw.model.UIPost
@Stable
interface PostActions {
@ -19,9 +18,9 @@ interface PostActions {
fun viewCommentsPage(commentsUrl: String)
fun toggleSave(post: SavedPost)
fun toggleSave(post: UIPost)
suspend fun getComments(postId: String): LobstersPostDetails
suspend fun getComments(postId: String): UIPost
suspend fun getLinkMetadata(url: String): LinkMetadata
}

View file

@ -9,7 +9,6 @@
package dev.msfjarvis.claw.model
import dev.drewhamilton.poko.Poko
import dev.msfjarvis.claw.database.local.SavedPost
import io.mcarle.konvert.api.KonvertTo
import io.mcarle.konvert.api.Mapping
import kotlinx.serialization.SerialName
@ -18,7 +17,7 @@ import kotlinx.serialization.Serializable
@Serializable
@Poko
@KonvertTo(
value = SavedPost::class,
value = UIPost::class,
mappings =
[
Mapping(target = "submitterName", expression = "it.submitter.username"),

View file

@ -17,6 +17,14 @@ import kotlinx.serialization.Serializable
@Serializable
@Poko
@KonvertTo(
value = UIPost::class,
mappings =
[
Mapping(target = "submitterName", expression = "it.submitter.username"),
Mapping(target = "submitterAvatarUrl", expression = "it.submitter.avatarUrl"),
],
)
@KonvertTo(
value = SavedPost::class,
mappings =

View file

@ -0,0 +1,50 @@
/*
* Copyright © 2021-2024 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 dev.msfjarvis.claw.database.local.SavedPost
import io.mcarle.konvert.api.KonvertFrom
import io.mcarle.konvert.api.KonvertTo
import io.mcarle.konvert.api.Mapping
import kotlinx.serialization.SerialName
@KonvertTo(
value = SavedPost::class,
mappings =
[
Mapping(target = "submitterName", expression = "it.submitter.username"),
Mapping(target = "submitterAvatarUrl", expression = "it.submitter.avatarUrl"),
],
)
data class UIPost(
val shortId: String,
val createdAt: String,
val title: String,
val url: String,
val description: String,
val commentCount: Int,
val commentsUrl: String,
@SerialName("submitter_user") val submitter: User,
val tags: List<String>,
val comments: List<Comment> = emptyList(),
val isSaved: Boolean = false,
val isRead: Boolean = false,
) {
@KonvertFrom(
value = SavedPost::class,
mappings =
[
Mapping(
target = "submitter",
expression = "User(it.submitterName, \"\", null, it.submitterAvatarUrl, \"\")",
),
Mapping(target = "commentCount", expression = "it.commentCount ?: 0"),
Mapping(target = "isSaved", expression = "true"),
],
)
companion object
}