diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt index 82b5c260..da0e18a2 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/LobstersPagingSource.kt @@ -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. @@ -13,8 +13,12 @@ import com.slack.eithernet.ApiResult.Success import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository +import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository import dev.msfjarvis.claw.core.injection.IODispatcher import dev.msfjarvis.claw.model.LobstersPost +import dev.msfjarvis.claw.model.UIPost +import dev.msfjarvis.claw.model.toUIPost import java.io.IOException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -24,15 +28,25 @@ class LobstersPagingSource constructor( @Assisted private val remoteFetcher: RemoteFetcher, @IODispatcher private val ioDispatcher: CoroutineDispatcher, -) : PagingSource() { + private val savedPostsRepository: SavedPostsRepository, + private val readPostsRepository: ReadPostsRepository, +) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { + override suspend fun load(params: LoadParams): LoadResult { val page = params.key ?: STARTING_PAGE_INDEX return when (val result = withContext(ioDispatcher) { remoteFetcher.getItemsAtPage(page) }) { is Success -> LoadResult.Page( itemsBefore = (page - 1) * PAGE_SIZE, - data = result.value, + data = + result.value.map { + it + .toUIPost() + .copy( + isSaved = savedPostsRepository.isPostSaved(it.shortId), + isRead = readPostsRepository.isPostRead(it.shortId), + ) + }, prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1, nextKey = page + 1, ) @@ -43,7 +57,7 @@ constructor( } } - override fun getRefreshKey(state: PagingState): Int? { + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> (anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX) } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/SearchPagingSource.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/SearchPagingSource.kt index c0f3988e..4b351a85 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/SearchPagingSource.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/paging/SearchPagingSource.kt @@ -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. @@ -14,9 +14,12 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.PAGE_SIZE import dev.msfjarvis.claw.android.paging.LobstersPagingSource.Companion.STARTING_PAGE_INDEX +import dev.msfjarvis.claw.android.viewmodel.ReadPostsRepository +import dev.msfjarvis.claw.android.viewmodel.SavedPostsRepository import dev.msfjarvis.claw.api.LobstersSearchApi import dev.msfjarvis.claw.core.injection.IODispatcher -import dev.msfjarvis.claw.model.LobstersPost +import dev.msfjarvis.claw.model.UIPost +import dev.msfjarvis.claw.model.toUIPost import java.io.IOException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -33,14 +36,16 @@ constructor( private val searchApi: LobstersSearchApi, @Assisted private val queryProvider: () -> String, @IODispatcher private val ioDispatcher: CoroutineDispatcher, -) : PagingSource() { - override fun getRefreshKey(state: PagingState): Int? { + private val savedPostsRepository: SavedPostsRepository, + private val readPostsRepository: ReadPostsRepository, +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> (anchorPosition / PAGE_SIZE).coerceAtLeast(STARTING_PAGE_INDEX) } } - override suspend fun load(params: LoadParams): LoadResult { + override suspend fun load(params: LoadParams): LoadResult { val searchQuery = queryProvider() // If there is no query, we don't need to call the API at all. if (searchQuery.isEmpty()) { @@ -55,7 +60,15 @@ constructor( val nextKey = if (result.value.isEmpty()) null else page + 1 LoadResult.Page( itemsBefore = (page - 1) * PAGE_SIZE, - data = result.value, + data = + result.value.map { + it + .toUIPost() + .copy( + isSaved = savedPostsRepository.isPostSaved(it.shortId), + isRead = readPostsRepository.isPostRead(it.shortId), + ) + }, prevKey = if (page == STARTING_PAGE_INDEX) null else page - 1, nextKey = nextKey, ) diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt index 1f3185c6..a2c72580 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ClawViewModel.kt @@ -18,9 +18,7 @@ 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 @@ -35,10 +33,8 @@ import dev.msfjarvis.claw.api.LobstersApi import dev.msfjarvis.claw.core.injection.IODispatcher import dev.msfjarvis.claw.core.injection.MainDispatcher 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 @@ -79,7 +75,6 @@ constructor( pagingSourceFactory = { pagingSourceFactory.create(api::getHottestPosts) }, ) .flow - .map(::mapToUIPost) .cachedIn(viewModelScope) val newestPosts = Pager( @@ -88,7 +83,6 @@ constructor( pagingSourceFactory = { pagingSourceFactory.create(api::getNewestPosts) }, ) .flow - .map(::mapToUIPost) .cachedIn(viewModelScope) val searchResults = Pager( @@ -97,7 +91,6 @@ constructor( pagingSourceFactory = { searchPagingSourceFactory.create { searchQuery } }, ) .flow - .map(::mapToUIPost) val savedPosts = savedPostsRepository.savedPosts.map { it.map(UIPost.Companion::fromSavedPost) } val savedPostsByMonth get() = savedPosts.map(::groupSavedPosts) @@ -112,13 +105,6 @@ constructor( viewModelScope.launch { readPostsRepository.readPosts.collectLatest { _readPosts = it } } } - private fun mapToUIPost(pagingData: PagingData): PagingData { - return pagingData.map { post -> - val uiPost = post.toUIPost() - uiPost.copy(isSaved = isPostSaved(uiPost), isRead = isPostRead(uiPost)) - } - } - private fun groupSavedPosts(items: List): ImmutableMap> { val sorted = items.sortedWith { post1, post2 -> @@ -140,22 +126,9 @@ constructor( .toImmutableMap() } - private fun isPostSaved(post: UIPost): Boolean { - return _savedPosts.contains(post.shortId) - } - - private fun isPostRead(post: UIPost): Boolean { - return _readPosts.contains(post.shortId) - } - fun toggleSave(post: UIPost) { - viewModelScope.launch(ioDispatcher) { - val saved = isPostSaved(post) - if (saved) { - savedPostsRepository.removePost(post.toSavedPost()) - } else { - savedPostsRepository.savePost(post.toSavedPost()) - } + viewModelScope.launch { + savedPostsRepository.toggleSave(post) withContext(mainDispatcher) { SavedPostsWidget(savedPosts).updateAll(getApplication()) } } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ReadPostsRepository.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ReadPostsRepository.kt index 2b5606a1..de236c31 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ReadPostsRepository.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/ReadPostsRepository.kt @@ -25,4 +25,8 @@ constructor( suspend fun markRead(postId: String) { withContext(dbDispatcher) { readPostsQueries.markRead(postId) } } + + suspend fun isPostRead(shortId: String): Boolean { + return withContext(dbDispatcher) { readPostsQueries.isPostRead(shortId).executeAsOne() } + } } diff --git a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt index 7886ca6a..15ee0728 100644 --- a/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt +++ b/android/src/main/kotlin/dev/msfjarvis/claw/android/viewmodel/SavedPostsRepository.kt @@ -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. @@ -11,6 +11,8 @@ import app.cash.sqldelight.coroutines.mapToList import dev.msfjarvis.claw.core.injection.DatabaseDispatcher import dev.msfjarvis.claw.database.local.SavedPost import dev.msfjarvis.claw.database.local.SavedPostQueries +import dev.msfjarvis.claw.model.UIPost +import dev.msfjarvis.claw.model.toSavedPost import io.github.aakira.napier.Napier import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -24,9 +26,14 @@ constructor( ) { val savedPosts = savedPostQueries.selectAllPosts().asFlow().mapToList(dbDispatcher) - suspend fun savePost(post: SavedPost) { - Napier.d(tag = TAG) { "Saving post: ${post.shortId}" } - withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post) } + suspend fun toggleSave(post: UIPost) { + if (post.isSaved) { + Napier.d(tag = TAG) { "Removing post: ${post.shortId}" } + withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) } + } else { + Napier.d(tag = TAG) { "Saving post: ${post.shortId}" } + withContext(dbDispatcher) { savedPostQueries.insertOrReplacePost(post.toSavedPost()) } + } } suspend fun savePosts(posts: List) { @@ -38,9 +45,8 @@ constructor( } } - suspend fun removePost(post: SavedPost) { - Napier.d(tag = TAG) { "Removing post: ${post.shortId}" } - withContext(dbDispatcher) { savedPostQueries.deletePost(post.shortId) } + suspend fun isPostSaved(postId: String): Boolean { + return withContext(dbDispatcher) { savedPostQueries.isPostSaved(postId).executeAsOne() } } private companion object { diff --git a/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/ReadPosts.sq b/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/ReadPosts.sq index f2398922..a76b0b2f 100644 --- a/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/ReadPosts.sq +++ b/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/ReadPosts.sq @@ -2,6 +2,13 @@ CREATE TABLE ReadPosts( id TEXT NOT NULL PRIMARY KEY ); +isPostRead: +SELECT EXISTS( + SELECT 1 + FROM ReadPosts + WHERE id = ? +) AS isRead; + selectAllPosts: SELECT * FROM ReadPosts; diff --git a/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/SavedPost.sq b/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/SavedPost.sq index 84dec796..b31bea9f 100644 --- a/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/SavedPost.sq +++ b/database/core/src/main/sqldelight/dev/msfjarvis/claw/database/local/SavedPost.sq @@ -14,6 +14,13 @@ CREATE TABLE IF NOT EXISTS SavedPost( description TEXT NOT NULL DEFAULT "" ); +isPostSaved: +SELECT EXISTS( + SELECT 1 + FROM SavedPost + WHERE shortId = ? +) AS isSaved; + insertOrReplacePost: INSERT OR REPLACE INTO SavedPost