mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-14 19:57:04 +05:30
refactor(android): switch over to PagingData transformations
This commit is contained in:
parent
051b7ab2bb
commit
74a7835a53
19 changed files with 187 additions and 151 deletions
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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 =
|
||||
|
|
50
model/src/main/kotlin/dev/msfjarvis/claw/model/UIPost.kt
Normal file
50
model/src/main/kotlin/dev/msfjarvis/claw/model/UIPost.kt
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue