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 androidx.glance.text.TextStyle
import dev.msfjarvis.claw.common.theme.DarkThemeColors import dev.msfjarvis.claw.common.theme.DarkThemeColors
import dev.msfjarvis.claw.common.theme.LightThemeColors 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.ImmutableList
import kotlinx.collections.immutable.toImmutableList 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) { override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent { provideContent {
GlanceTheme( GlanceTheme(
@ -46,7 +46,7 @@ class SavedPostsWidget(private val posts: List<SavedPost>) : GlanceAppWidget() {
} }
@Composable @Composable
fun WidgetHost(posts: ImmutableList<SavedPost>, modifier: GlanceModifier = GlanceModifier) { fun WidgetHost(posts: ImmutableList<UIPost>, modifier: GlanceModifier = GlanceModifier) {
LazyColumn( LazyColumn(
modifier = modifier =
modifier.fillMaxSize().background(GlanceTheme.colors.background).appWidgetBackground(), 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
import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY import dev.msfjarvis.claw.android.MainActivity.Companion.NAVIGATION_KEY
import dev.msfjarvis.claw.android.R 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) private val destinationKey = Key<String>(NAVIGATION_KEY)
@Composable @Composable
@GlanceComposable @GlanceComposable
fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier) { fun WidgetListEntry(post: UIPost, modifier: GlanceModifier = GlanceModifier) {
val titleStyle = MaterialTheme.typography.titleMedium val titleStyle = MaterialTheme.typography.titleMedium
val commentsAction = val commentsAction =
actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId)) actionStartActivity<MainActivity>(actionParametersOf(destinationKey to post.shortId))
@ -68,7 +68,7 @@ fun WidgetListEntry(post: SavedPost, modifier: GlanceModifier = GlanceModifier)
) )
Image( Image(
provider = ImageProvider(R.drawable.ic_comment), provider = ImageProvider(R.drawable.ic_comment),
contentDescription = "${post.commentCount ?: 0} comments", contentDescription = "${post.commentCount} comments",
modifier = GlanceModifier.padding(end = 4.dp).clickable(commentsAction), 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * 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.android.viewmodel.ClawViewModel
import dev.msfjarvis.claw.common.posts.PostActions import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.urllauncher.UrlLauncher 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.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.UIPost
fun Context.getActivity(): ComponentActivity? { fun Context.getActivity(): ComponentActivity? {
return when (this) { return when (this) {
@ -59,11 +58,11 @@ fun rememberPostActions(
urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r")) urlLauncher.openUri(commentsUrl.replaceAfterLast('/', "r"))
} }
override fun toggleSave(post: SavedPost) { override fun toggleSave(post: UIPost) {
viewModel.toggleSave(post) viewModel.toggleSave(post)
} }
override suspend fun getComments(postId: String): LobstersPostDetails { override suspend fun getComments(postId: String): UIPost {
return viewModel.getPostComments(postId) return viewModel.getPostComments(postId)
} }

View file

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

View file

@ -11,24 +11,16 @@ import androidx.compose.material.icons.automirrored.filled.Reply
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.graphics.vector.rememberVectorPainter
import dev.msfjarvis.claw.common.posts.LobstersCard import dev.msfjarvis.claw.common.posts.LobstersCard
import dev.msfjarvis.claw.common.posts.PostActions 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.SwipeAction
import me.saket.swipe.SwipeableActionsBox import me.saket.swipe.SwipeableActionsBox
@Composable @Composable
fun LobstersListItem( fun LobstersListItem(item: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
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) }
val commentsAction = val commentsAction =
SwipeAction( SwipeAction(
icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply), icon = rememberVectorPainter(Icons.AutoMirrored.Filled.Reply),
@ -36,12 +28,6 @@ fun LobstersListItem(
onSwipe = { postActions.viewCommentsPage(item.commentsUrl) }, onSwipe = { postActions.viewCommentsPage(item.commentsUrl) },
) )
SwipeableActionsBox(endActions = listOf(commentsAction)) { SwipeableActionsBox(endActions = listOf(commentsAction)) {
LobstersCard( LobstersCard(post = item, postActions = postActions, modifier = modifier)
post = item,
isSaved = isSaved(item),
isRead = read,
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.posts.PostActions
import dev.msfjarvis.claw.common.ui.NetworkError import dev.msfjarvis.claw.common.ui.NetworkError
import dev.msfjarvis.claw.common.ui.ProgressBar import dev.msfjarvis.claw.common.ui.ProgressBar
import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.LobstersPost
import dev.msfjarvis.claw.model.toSavedPost
import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator import eu.bambooapps.material3.pullrefresh.PullRefreshIndicator
import eu.bambooapps.material3.pullrefresh.pullRefresh import eu.bambooapps.material3.pullrefresh.pullRefresh
import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
@ -37,10 +35,8 @@ import eu.bambooapps.material3.pullrefresh.rememberPullRefreshState
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NetworkPosts( fun NetworkPosts(
lazyPagingItems: LazyPagingItems<LobstersPost>, lazyPagingItems: LazyPagingItems<UIPost>,
listState: LazyListState, listState: LazyListState,
isPostSaved: (SavedPost) -> Boolean,
isPostRead: suspend (String) -> Boolean,
postActions: PostActions, postActions: PostActions,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -64,13 +60,7 @@ fun NetworkPosts(
) { index -> ) { index ->
val item = lazyPagingItems[index] val item = lazyPagingItems[index]
if (item != null) { if (item != null) {
val dbModel = item.toSavedPost() LobstersListItem(item = item, postActions = postActions)
LobstersListItem(
item = dbModel,
isSaved = isPostSaved,
isRead = isPostRead,
postActions = postActions,
)
HorizontalDivider() 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * https://opensource.org/licenses/MIT.
@ -21,15 +21,13 @@ import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import dev.msfjarvis.claw.common.posts.PostActions import dev.msfjarvis.claw.common.posts.PostActions
import dev.msfjarvis.claw.common.ui.SearchBar import dev.msfjarvis.claw.common.ui.SearchBar
import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.LobstersPost
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@Composable @Composable
fun SearchList( fun SearchList(
items: Flow<PagingData<LobstersPost>>, items: Flow<PagingData<UIPost>>,
listState: LazyListState, listState: LazyListState,
isPostSaved: (SavedPost) -> Boolean,
postActions: PostActions, postActions: PostActions,
searchQuery: String, searchQuery: String,
setSearchQuery: (String) -> Unit, setSearchQuery: (String) -> Unit,
@ -50,8 +48,6 @@ fun SearchList(
NetworkPosts( NetworkPosts(
lazyPagingItems = lazyPagingItems, lazyPagingItems = lazyPagingItems,
listState = listState, listState = listState,
isPostSaved = isPostSaved,
isPostRead = { false },
postActions = postActions, postActions = postActions,
) )
} }

View file

@ -228,8 +228,6 @@ fun LobstersPostsScreen(
NetworkPosts( NetworkPosts(
lazyPagingItems = hottestPosts, lazyPagingItems = hottestPosts,
listState = hottestListState, listState = hottestListState,
isPostSaved = viewModel::isPostSaved,
isPostRead = viewModel::isPostRead,
postActions = postActions, postActions = postActions,
) )
} }
@ -238,8 +236,6 @@ fun LobstersPostsScreen(
NetworkPosts( NetworkPosts(
lazyPagingItems = newestPosts, lazyPagingItems = newestPosts,
listState = newestListState, listState = newestListState,
isPostSaved = viewModel::isPostSaved,
isPostRead = viewModel::isPostRead,
postActions = postActions, 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * https://opensource.org/licenses/MIT.
@ -47,7 +47,6 @@ fun SearchScreen(
SearchList( SearchList(
items = viewModel.searchResults, items = viewModel.searchResults,
listState = listState, listState = listState,
isPostSaved = viewModel::isPostSaved,
postActions = postActions, postActions = postActions,
searchQuery = viewModel.searchQuery, searchQuery = viewModel.searchQuery,
setSearchQuery = { query -> viewModel.searchQuery = query }, setSearchQuery = { query -> viewModel.searchQuery = query },

View file

@ -18,6 +18,9 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig 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.app.ApplicationScope
import com.deliveryhero.whetstone.viewmodel.ContributesViewModel import com.deliveryhero.whetstone.viewmodel.ContributesViewModel
import com.slack.eithernet.ApiResult.Failure 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.api.LobstersApi
import dev.msfjarvis.claw.core.injection.IODispatcher import dev.msfjarvis.claw.core.injection.IODispatcher
import dev.msfjarvis.claw.core.injection.MainDispatcher 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.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.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@ -44,9 +51,11 @@ import javax.inject.Inject
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -68,34 +77,43 @@ constructor(
@MainDispatcher private val mainDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher,
@ForScope(ApplicationScope::class) context: Context, @ForScope(ApplicationScope::class) context: Context,
) : AndroidViewModel(context as Application) { ) : AndroidViewModel(context as Application) {
private val hottestPostsPager = val hottestPosts =
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) { Pager(
pagingSourceFactory.create(api::getHottestPosts) config = PagingConfig(pageSize = PAGE_SIZE),
} initialKey = STARTING_PAGE_INDEX,
private val newestPostsPager = pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) },
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) { )
pagingSourceFactory.create(api::getNewestPosts) .flow
} .map(::mapUIPost)
private val searchResultsPager = .cachedIn(viewModelScope)
Pager(PagingConfig(pageSize = PAGE_SIZE), initialKey = STARTING_PAGE_INDEX) {
searchPagingSourceFactory.create { searchQuery }
}
val hottestPosts val newestPosts =
get() = hottestPostsPager.flow Pager(
config = PagingConfig(pageSize = PAGE_SIZE),
val newestPosts initialKey = STARTING_PAGE_INDEX,
get() = newestPostsPager.flow 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 val savedPosts
get() = savedPostsRepository.savedPosts get() =
savedPostsRepository.savedPosts
.map { it.map(UIPost.Companion::fromSavedPost) }
.shareIn(viewModelScope, started = SharingStarted.Lazily, Int.MAX_VALUE)
val savedPostsByMonth val savedPostsByMonth
get() = savedPosts.map(::mapSavedPosts) get() = savedPosts.map(::mapSavedPosts)
val searchResults
get() = searchResultsPager.flow
var searchQuery by mutableStateOf("") var searchQuery by mutableStateOf("")
private val _savedPostsMutex = Mutex() private val _savedPostsMutex = Mutex()
@ -104,12 +122,19 @@ constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
savedPosts.collectLatest { 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 = val sorted =
items.sortedWith { post1, post2 -> items.sortedWith { post1, post2 ->
val post1Date = post1.createdAt.toLocalDateTime() val post1Date = post1.createdAt.toLocalDateTime()
@ -130,17 +155,19 @@ constructor(
.toImmutableMap() .toImmutableMap()
} }
fun isPostSaved(post: SavedPost): Boolean { private fun isPostSaved(post: UIPost): Boolean {
return _savedPosts.contains(post.shortId) 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) { viewModelScope.launch(ioDispatcher) {
val saved = isPostSaved(post) val saved = isPostSaved(post)
if (saved) { if (saved) {
savedPostsRepository.removePost(post) savedPostsRepository.removePost(post.toSavedPost())
} else { } else {
savedPostsRepository.savePost(post) savedPostsRepository.savePost(post.toSavedPost())
} }
val newPosts = savedPosts.first() val newPosts = savedPosts.first()
withContext(mainDispatcher) { withContext(mainDispatcher) {
@ -152,7 +179,7 @@ constructor(
suspend fun getPostComments(postId: String) = suspend fun getPostComments(postId: String) =
withContext(ioDispatcher) { withContext(ioDispatcher) {
when (val result = api.getPostDetails(postId)) { when (val result = api.getPostDetails(postId)) {
is Success -> result.value is Success -> result.value.toUIPost()
is Failure.NetworkFailure -> throw result.error is Failure.NetworkFailure -> throw result.error
is Failure.UnknownFailure -> throw result.error is Failure.UnknownFailure -> throw result.error
is Failure.HttpFailure -> { is Failure.HttpFailure -> {
@ -198,8 +225,6 @@ constructor(
viewModelScope.launch { readPostsRepository.markRead(postId) } 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 * 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 * 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * https://opensource.org/licenses/MIT.
@ -23,6 +23,7 @@ constructor(
withContext(dbDispatcher) { readPostsQueries.markRead(postId) } withContext(dbDispatcher) { readPostsQueries.markRead(postId) }
} }
suspend fun isRead(postId: String): Boolean = fun isRead(postId: String): Boolean {
withContext(dbDispatcher) { readPostsQueries.isRead(postId).executeAsOneOrNull() != null } 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * 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.android.viewmodel.SavedPostsRepository
import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.api.LobstersApi
import dev.msfjarvis.claw.model.LobstersPostDetails 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 dev.msfjarvis.claw.model.toSavedPost
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -44,7 +46,10 @@ constructor(
.filterIsInstance<Success<LobstersPostDetails>>() .filterIsInstance<Success<LobstersPostDetails>>()
.map { result -> result.value.toSavedPost() } .map { result -> result.value.toSavedPost() }
.let { savedPostsRepository.savePosts(it) } .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() 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.NetworkImage
import dev.msfjarvis.claw.common.ui.ThemedRichText import dev.msfjarvis.claw.common.ui.ThemedRichText
import dev.msfjarvis.claw.model.LinkMetadata 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.Instant
import java.time.temporal.TemporalAccessor import java.time.temporal.TemporalAccessor
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
internal fun CommentsHeader( internal fun CommentsHeader(
postDetails: LobstersPostDetails, post: UIPost,
postActions: PostActions, postActions: PostActions,
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val linkMetadata by val linkMetadata by
produceState(initialValue = LinkMetadata(postDetails.url, null)) { produceState(initialValue = LinkMetadata(post.url, null)) {
runSuspendCatching { postActions.getLinkMetadata(postDetails.url) } runSuspendCatching { postActions.getLinkMetadata(post.url) }
.onSuccess { metadata -> value = metadata } .onSuccess { metadata -> value = metadata }
} }
@ -70,8 +70,8 @@ internal fun CommentsHeader(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
PostTitle(title = postDetails.title, isRead = false) PostTitle(title = post.title, isRead = false)
TagRow(tags = postDetails.tags.toImmutableList()) TagRow(tags = post.tags.toImmutableList())
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
if (linkMetadata.url.isNotBlank()) { if (linkMetadata.url.isNotBlank()) {
@ -79,23 +79,23 @@ internal fun CommentsHeader(
linkMetadata = linkMetadata, linkMetadata = linkMetadata,
modifier = modifier =
Modifier.clickable { Modifier.clickable {
postActions.viewPost(postDetails.shortId, linkMetadata.url, postDetails.commentsUrl) postActions.viewPost(post.shortId, linkMetadata.url, post.commentsUrl)
}, },
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
} }
if (postDetails.description.isNotBlank()) { if (post.description.isNotBlank()) {
ThemedRichText(htmlConverter.convertHTMLToMarkdown(postDetails.description)) ThemedRichText(htmlConverter.convertHTMLToMarkdown(post.description))
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
} }
Submitter( Submitter(
text = AnnotatedString("Submitted by ${postDetails.submitter.username}"), text = AnnotatedString("Submitted by ${post.submitter.username}"),
avatarUrl = "https://lobste.rs/${postDetails.submitter.avatarUrl}", avatarUrl = "https://lobste.rs/${post.submitter.avatarUrl}",
contentDescription = "User avatar for ${postDetails.submitter.username}", contentDescription = "User avatar for ${post.submitter.username}",
modifier = modifier =
Modifier.clickable { 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * 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.common.ui.ProgressBar
import dev.msfjarvis.claw.database.local.PostComments import dev.msfjarvis.claw.database.local.PostComments
import dev.msfjarvis.claw.model.Comment import dev.msfjarvis.claw.model.Comment
import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.UIPost
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
private fun CommentsPageInternal( private fun CommentsPageInternal(
details: LobstersPostDetails, details: UIPost,
postActions: PostActions, postActions: PostActions,
htmlConverter: HTMLConverter, htmlConverter: HTMLConverter,
commentState: PostComments?, commentState: PostComments?,
@ -54,11 +54,7 @@ private fun CommentsPageInternal(
Surface(color = MaterialTheme.colorScheme.surfaceVariant) { Surface(color = MaterialTheme.colorScheme.surfaceVariant) {
LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) { LazyColumn(modifier = modifier, contentPadding = PaddingValues(bottom = 24.dp)) {
item { item {
CommentsHeader( CommentsHeader(post = details, postActions = postActions, htmlConverter = htmlConverter)
postDetails = details,
postActions = postActions,
htmlConverter = htmlConverter,
)
} }
if (commentNodes.isNotEmpty()) { if (commentNodes.isNotEmpty()) {
@ -123,7 +119,7 @@ fun CommentsPage(
when (postDetails) { when (postDetails) {
is Success<*> -> { is Success<*> -> {
CommentsPageInternal( CommentsPageInternal(
details = (postDetails as Success<LobstersPostDetails>).data, details = (postDetails as Success<UIPost>).data,
postActions = postActions, postActions = postActions,
htmlConverter = htmlConverter, htmlConverter = htmlConverter,
commentState = commentState, 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.theme.LobstersTheme
import dev.msfjarvis.claw.common.ui.NetworkImage import dev.msfjarvis.claw.common.ui.NetworkImage
import dev.msfjarvis.claw.common.ui.preview.ThemePreviews 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.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.UIPost
import dev.msfjarvis.claw.model.User import dev.msfjarvis.claw.model.User
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun LobstersCard( fun LobstersCard(post: UIPost, postActions: PostActions, modifier: Modifier = Modifier) {
post: SavedPost, var localSavedState by remember(post) { mutableStateOf(post.isSaved) }
isSaved: Boolean,
isRead: Boolean,
postActions: PostActions,
modifier: Modifier = Modifier,
) {
var localSavedState by remember(post, isSaved) { mutableStateOf(isSaved) }
Box( Box(
modifier = modifier =
modifier modifier
@ -79,7 +72,7 @@ fun LobstersCard(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
PostDetails(modifier = Modifier.weight(1f), post = post, isRead = isRead) PostDetails(modifier = Modifier.weight(1f), post = post)
Column( Column(
modifier = Modifier.wrapContentHeight(), modifier = Modifier.wrapContentHeight(),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@ -108,15 +101,15 @@ fun LobstersCard(
} }
@Composable @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)) { 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()) TagRow(tags = post.tags.toImmutableList())
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Submitter( Submitter(
text = AnnotatedString("Submitted by ${post.submitterName}"), text = AnnotatedString("Submitted by ${post.submitter.username}"),
avatarUrl = "https://lobste.rs/${post.submitterAvatarUrl}", avatarUrl = "https://lobste.rs/${post.submitter.avatarUrl}",
contentDescription = "User avatar for ${post.submitterName}", contentDescription = "User avatar for ${post.submitter.username}",
) )
} }
} }
@ -234,20 +227,19 @@ private fun LobstersCardPreview() {
LobstersTheme { LobstersTheme {
LobstersCard( LobstersCard(
post = post =
SavedPost( UIPost(
shortId = "ooga", shortId = "ooga",
title = "Simple Anomaly Detection Using Plain SQL", title = "Simple Anomaly Detection Using Plain SQL",
url = "https://hakibenita.com/sql-anomaly-detection", url = "https://hakibenita.com/sql-anomaly-detection",
createdAt = "2020-09-21T08:04:24.000-05:00", createdAt = "2020-09-21T08:04:24.000-05:00",
commentCount = 1, commentCount = 1,
commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql", commentsUrl = "https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql",
submitterName = "Haki", submitter = User("Haki", "", "", "/avatars/Haki-100.png", ""),
submitterAvatarUrl = "/avatars/Haki-100.png",
tags = listOf("databases", "apis"), tags = listOf("databases", "apis"),
description = "", description = "",
isSaved = true,
isRead = true,
), ),
isRead = true,
isSaved = true,
postActions = postActions =
object : PostActions { object : PostActions {
override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {} override fun viewPost(postId: String, postUrl: String, commentsUrl: String) {}
@ -256,10 +248,10 @@ private fun LobstersCardPreview() {
override fun viewCommentsPage(commentsUrl: String) {} override fun viewCommentsPage(commentsUrl: String) {}
override fun toggleSave(post: SavedPost) {} override fun toggleSave(post: UIPost) {}
override suspend fun getComments(postId: String): LobstersPostDetails { override suspend fun getComments(postId: String): UIPost {
return LobstersPostDetails( return UIPost(
shortId = "ooga", shortId = "ooga",
title = "Simple Anomaly Detection Using Plain SQL", title = "Simple Anomaly Detection Using Plain SQL",
url = "https://hakibenita.com/sql-anomaly-detection", 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 * Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at * license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT. * https://opensource.org/licenses/MIT.
@ -7,9 +7,8 @@
package dev.msfjarvis.claw.common.posts package dev.msfjarvis.claw.common.posts
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import dev.msfjarvis.claw.database.local.SavedPost
import dev.msfjarvis.claw.model.LinkMetadata import dev.msfjarvis.claw.model.LinkMetadata
import dev.msfjarvis.claw.model.LobstersPostDetails import dev.msfjarvis.claw.model.UIPost
@Stable @Stable
interface PostActions { interface PostActions {
@ -19,9 +18,9 @@ interface PostActions {
fun viewCommentsPage(commentsUrl: String) 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 suspend fun getLinkMetadata(url: String): LinkMetadata
} }

View file

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

View file

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