diff --git a/domain/lemmy/data/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/lemmy/data/SearchResult.kt b/domain/lemmy/data/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/lemmy/data/SearchResult.kt index 0b2dcff83..8ae978f2f 100644 --- a/domain/lemmy/data/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/lemmy/data/SearchResult.kt +++ b/domain/lemmy/data/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/domain/lemmy/data/SearchResult.kt @@ -9,3 +9,12 @@ sealed interface SearchResult { data class Community(val model: CommunityModel) : SearchResult } + +val SearchResult.uniqueIdentifier: String + get() = + when (this) { + is SearchResult.Post -> "post" + model.id.toString() + model.updateDate + is SearchResult.Comment -> "comment" + model.id.toString() + model.updateDate + is SearchResult.User -> "user" + model.id.toString() + is SearchResult.Community -> "community" + model.id.toString() + } diff --git a/domain/lemmy/pagination/src/androidUnitTest/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultExplorePaginationManagerTest.kt b/domain/lemmy/pagination/src/androidUnitTest/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultExplorePaginationManagerTest.kt new file mode 100644 index 000000000..f7dac3807 --- /dev/null +++ b/domain/lemmy/pagination/src/androidUnitTest/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultExplorePaginationManagerTest.kt @@ -0,0 +1,221 @@ +package com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination + +import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository +import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DomainBlocklistRepository +import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.StopWordRepository +import com.github.diegoberaldin.raccoonforlemmy.core.testutils.DispatcherTestRule +import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifySequence +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class DefaultExplorePaginationManagerTest { + @get:Rule + val dispatcherTestRule = DispatcherTestRule() + + private val identityRepository: IdentityRepository = + mockk { + every { authToken } returns MutableStateFlow(AUTH_TOKEN) + every { cachedUser } returns UserModel(id = 1) + } + private val communityRepository: CommunityRepository = mockk(relaxUnitFun = true) + private val userRepository: UserRepository = mockk(relaxUnitFun = true) + private val accountRepository = + mockk(relaxUnitFun = true) { + coEvery { getActive() } returns null + } + private val domainBlocklistRepository = + mockk(relaxUnitFun = true) { + coEvery { get(accountId = any()) } returns emptyList() + } + private val stopWordRepository = + mockk(relaxUnitFun = true) { + coEvery { get(accountId = any()) } returns emptyList() + } + + private val sut = + DefaultExplorePaginationManager( + identityRepository = identityRepository, + accountRepository = accountRepository, + communityRepository = communityRepository, + userRepository = userRepository, + domainBlocklistRepository = domainBlocklistRepository, + stopWordRepository = stopWordRepository, + ) + + @Test + fun whenReset_thenCanFetchMore() = + runTest { + val specification = ExplorePaginationSpecification() + sut.reset(specification) + + assertTrue(sut.canFetchMore) + } + + @Test + fun givenNoResults_whenLoadNextPage_thenResultIsAsExpected() = + runTest { + coEvery { + communityRepository.search( + query = any(), + auth = any(), + page = any(), + limit = any(), + sortType = any(), + listingType = any(), + resultType = any(), + instance = any(), + communityId = any(), + ) + } returns emptyList() + val specification = ExplorePaginationSpecification() + sut.reset(specification) + + val items = sut.loadNextPage() + + assertTrue(items.isEmpty()) + coVerify { + communityRepository.search( + auth = AUTH_TOKEN, + page = 1, + limit = 20, + listingType = specification.listingType, + sortType = specification.sortType, + communityId = null, + instance = null, + resultType = specification.resultType, + query = "", + ) + } + } + + @Test + fun givenResults_whenLoadNextPage_thenResultIsAsExpected() = + runTest { + val page = slot() + coEvery { + communityRepository.search( + query = any(), + auth = any(), + page = capture(page), + limit = any(), + sortType = any(), + listingType = any(), + resultType = any(), + instance = any(), + communityId = any(), + ) + } answers { + val pageNumber = page.captured + if (pageNumber == 1) { + (0..<20).map { idx -> + SearchResult.Post(PostModel(id = idx.toLong())) + } + } else { + emptyList() + } + } + val specification = ExplorePaginationSpecification() + sut.reset(specification) + + val items = sut.loadNextPage() + + assertEquals(20, items.size) + assertTrue(sut.canFetchMore) + + coVerify { + communityRepository.search( + auth = AUTH_TOKEN, + page = 1, + limit = 20, + listingType = specification.listingType, + sortType = specification.sortType, + communityId = null, + instance = null, + resultType = specification.resultType, + query = "", + ) + } + } + + @Test + fun givenResults_whenSecondLoadNextPage_thenResultIsAsExpected() = + runTest { + val page = slot() + coEvery { + communityRepository.search( + query = any(), + auth = any(), + page = capture(page), + limit = any(), + sortType = any(), + listingType = any(), + resultType = any(), + instance = any(), + communityId = any(), + ) + } answers { + val pageNumber = page.captured + if (pageNumber == 1) { + (0..<20).map { idx -> + SearchResult.Post(PostModel(id = idx.toLong())) + } + } else { + emptyList() + } + } + val specification = ExplorePaginationSpecification() + sut.reset(specification) + + sut.loadNextPage() + val items = sut.loadNextPage() + + assertEquals(20, items.size) + assertFalse(sut.canFetchMore) + + coVerifySequence { + communityRepository.search( + auth = AUTH_TOKEN, + page = 1, + limit = 20, + listingType = specification.listingType, + sortType = specification.sortType, + communityId = null, + instance = null, + resultType = specification.resultType, + query = "", + ) + + communityRepository.search( + auth = AUTH_TOKEN, + page = 2, + limit = 20, + listingType = specification.listingType, + sortType = specification.sortType, + communityId = null, + instance = null, + resultType = specification.resultType, + query = "", + ) + } + } + + companion object { + private const val AUTH_TOKEN = "fake-token" + } +} diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommentPaginationManager.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommentPaginationManager.kt index c86dfc7c5..b1ca55ace 100644 --- a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommentPaginationManager.kt +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommentPaginationManager.kt @@ -24,7 +24,6 @@ internal class DefaultCommentPaginationManager( dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : CommentPaginationManager { override var canFetchMore: Boolean = true - private set private var specification: CommentPaginationSpecification? = null private var currentPage: Int = 1 diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommunityPaginationManager.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommunityPaginationManager.kt index 9187120f9..70207c3c9 100644 --- a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommunityPaginationManager.kt +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultCommunityPaginationManager.kt @@ -17,7 +17,6 @@ internal class DefaultCommunityPaginationManager( private val communityRepository: CommunityRepository, ) : CommunityPaginationManager { override var canFetchMore: Boolean = true - private set override val history: MutableList = mutableListOf() private var specification: CommunityPaginationSpecification? = null diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultExplorePaginationManager.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultExplorePaginationManager.kt new file mode 100644 index 000000000..d902605d8 --- /dev/null +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultExplorePaginationManager.kt @@ -0,0 +1,216 @@ +package com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination + +import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository +import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DomainBlocklistRepository +import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.StopWordRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.uniqueIdentifier +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.withContext + +class DefaultExplorePaginationManager( + private val identityRepository: IdentityRepository, + private val accountRepository: AccountRepository, + private val communityRepository: CommunityRepository, + private val userRepository: UserRepository, + private val domainBlocklistRepository: DomainBlocklistRepository, + private val stopWordRepository: StopWordRepository, +) : ExplorePaginationManager { + override var canFetchMore: Boolean = true + + private var specification: ExplorePaginationSpecification? = null + private var currentPage: Int = 1 + private val history: MutableList = mutableListOf() + private var blockedDomains: List? = null + private var stopWords: List? = null + + override fun reset(specification: ExplorePaginationSpecification) { + this.specification = specification + canFetchMore = true + currentPage = 1 + history.clear() + blockedDomains = null + stopWords = null + } + + override suspend fun loadNextPage(): List = + withContext(Dispatchers.IO) { + val specification = specification ?: return@withContext emptyList() + val auth = identityRepository.authToken.value.orEmpty() + val accountId = accountRepository.getActive()?.id + if (blockedDomains == null) { + blockedDomains = domainBlocklistRepository.get(accountId) + } + if (stopWords == null) { + stopWords = stopWordRepository.get(accountId) + } + + val searchText = specification.query.orEmpty() + val resultType = specification.resultType + val itemList: List = + communityRepository.search( + query = searchText, + auth = auth, + resultType = resultType, + page = currentPage, + listingType = specification.listingType, + sortType = specification.sortType, + instance = specification.otherInstance, + ) + val additionalResolvedCommunity = + if (resultType == SearchResultType.All || + resultType == SearchResultType.Communities && + currentPage == 1 && + searchText.isNotEmpty() + ) { + communityRepository.getResolved( + query = searchText, + auth = auth, + ) + } else { + null + } + val additionalResolvedUser = + if (resultType == SearchResultType.All || + resultType == SearchResultType.Users && + currentPage == 1 && + searchText.isNotEmpty() + ) { + userRepository.getResolved( + query = searchText, + auth = auth, + ) + } else { + null + } + + if (itemList.isNotEmpty()) { + currentPage++ + } + canFetchMore = itemList.isNotEmpty() + + val result = + itemList + .deduplicate() + .filterNsfw(specification.includeNsfw) + .filterDeleted() + .filterByUrlDomain() + .filterByStopWords() + .let { + when (resultType) { + SearchResultType.Communities -> { + if (additionalResolvedCommunity != null && + it.none { r -> + r is SearchResult.Community && r.model.id == additionalResolvedCommunity.id + } + ) { + it + SearchResult.Community(additionalResolvedCommunity) + } else { + it + } + } + + SearchResultType.Users -> { + if (additionalResolvedUser != null && + it.none { r -> + r is SearchResult.User && r.model.id == additionalResolvedUser.id + } + ) { + it + SearchResult.User(additionalResolvedUser) + } else { + it + } + } + + SearchResultType.Posts -> { + if (specification.searchPostTitleOnly && searchText.isNotEmpty()) { + // apply the more restrictive title-only search + it + .filterIsInstance() + .filter { r -> + r.model.title.contains( + other = searchText, + ignoreCase = true, + ) + } + } else { + it + } + } + + else -> it + } + } + + history.addAll(result) + // returns a copy of the whole history + history.map { it } + } + + private fun List.deduplicate(): List = + filter { c1 -> + // prevents accidental duplication + history.none { c2 -> c2.uniqueIdentifier == c1.uniqueIdentifier } + } + + private fun List.filterNsfw(includeNsfw: Boolean): List = + if (includeNsfw) { + this + } else { + filter { res -> + when (res) { + is SearchResult.Community -> !res.model.nsfw + is SearchResult.Post -> !res.model.nsfw + is SearchResult.Comment -> true + is SearchResult.User -> true + else -> false + } + } + } + + private fun List.filterDeleted(): List { + return filter { + when (it) { + is SearchResult.Post -> !it.model.deleted + is SearchResult.Comment -> !it.model.deleted + else -> true + } + } + } + + private fun List.filterByUrlDomain(): List = + filter { + when (it) { + is SearchResult.Post -> { + blockedDomains?.takeIf { l -> l.isNotEmpty() }?.let { blockList -> + blockList.none { domain -> it.model.url?.contains(domain) ?: true } + } ?: true + } + + else -> true + } + } + + private fun List.filterByStopWords(): List = + filter { + when (it) { + is SearchResult.Post -> { + stopWords?.takeIf { l -> l.isNotEmpty() }?.let { stopWordList -> + stopWordList.none { domain -> + it.model.title.contains( + other = domain, + ignoreCase = true, + ) + } + } ?: true + } + + else -> true + } + } +} diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultPostPaginationManager.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultPostPaginationManager.kt index d5f54ad9f..168ae2e51 100644 --- a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultPostPaginationManager.kt +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/DefaultPostPaginationManager.kt @@ -35,7 +35,6 @@ internal class DefaultPostPaginationManager( notificationCenter: NotificationCenter, ) : PostPaginationManager { override var canFetchMore: Boolean = true - private set override val history: MutableList = mutableListOf() private var specification: PostPaginationSpecification? = null @@ -301,16 +300,15 @@ internal class DefaultPostPaginationManager( } } - private fun List.filterByUrlDomain(): List { - return filter { post -> + private fun List.filterByUrlDomain(): List = + filter { post -> blockedDomains?.takeIf { it.isNotEmpty() }?.let { blockList -> blockList.none { domain -> post.url?.contains(domain) ?: true } } ?: true } - } - private fun List.filterByStopWords(): List { - return filter { post -> + private fun List.filterByStopWords(): List = + filter { post -> stopWords?.takeIf { it.isNotEmpty() }?.let { stopWordList -> stopWordList.none { domain -> post.title.contains( @@ -320,7 +318,6 @@ internal class DefaultPostPaginationManager( } } ?: true } - } private fun handlePostUpdate(post: PostModel) { val index = history.indexOfFirst { it.id == post.id }.takeIf { it >= 0 } ?: return diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/ExplorePaginationManager.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/ExplorePaginationManager.kt new file mode 100644 index 000000000..1fb2bcae1 --- /dev/null +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/ExplorePaginationManager.kt @@ -0,0 +1,11 @@ +package com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination + +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult + +interface ExplorePaginationManager { + val canFetchMore: Boolean + + fun reset(specification: ExplorePaginationSpecification) + + suspend fun loadNextPage(): List +} diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/ExplorePaginationSpecification.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/ExplorePaginationSpecification.kt new file mode 100644 index 000000000..c15d7f5d3 --- /dev/null +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/ExplorePaginationSpecification.kt @@ -0,0 +1,15 @@ +package com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination + +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType + +data class ExplorePaginationSpecification( + val resultType: SearchResultType = SearchResultType.Communities, + val listingType: ListingType = ListingType.All, + val sortType: SortType = SortType.Active, + val includeNsfw: Boolean = true, + val searchPostTitleOnly: Boolean = false, + val otherInstance: String? = null, + val query: String? = null, +) diff --git a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/di/LemmyPaginationModule.kt b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/di/LemmyPaginationModule.kt index 5560eb683..17b691035 100644 --- a/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/di/LemmyPaginationModule.kt +++ b/domain/lemmy/pagination/src/commonMain/kotlin/com/diegoberaldin/raccoonforlemmy/domain/lemmy/pagination/di/LemmyPaginationModule.kt @@ -4,9 +4,11 @@ import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.CommentPaginati import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.CommunityPaginationManager import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.DefaultCommentPaginationManager import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.DefaultCommunityPaginationManager +import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.DefaultExplorePaginationManager import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.DefaultMultiCommunityPaginator import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.DefaultPostNavigationManager import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.DefaultPostPaginationManager +import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.ExplorePaginationManager import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.MultiCommunityPaginator import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.PostNavigationManager import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.PostPaginationManager @@ -51,4 +53,14 @@ val paginationModule = communityRepository = get(), ) } + factory { + DefaultExplorePaginationManager( + identityRepository = get(), + accountRepository = get(), + communityRepository = get(), + userRepository = get(), + domainBlocklistRepository = get(), + stopWordRepository = get(), + ) + } } diff --git a/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreScreen.kt b/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreScreen.kt index b16015aee..bf107fcd3 100644 --- a/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreScreen.kt +++ b/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreScreen.kt @@ -82,6 +82,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.readableHandle import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toInt +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.uniqueIdentifier import com.github.diegoberaldin.raccoonforlemmy.unit.explore.components.ExploreTopBar import com.github.diegoberaldin.raccoonforlemmy.unit.web.WebViewScreen import com.github.diegoberaldin.raccoonforlemmy.unit.zoomableimage.ZoomableImageScreen @@ -307,7 +308,7 @@ class ExploreScreen( } } } - items(uiState.results, key = { getItemKey(it) }) { result -> + items(uiState.results, key = { it.uniqueIdentifier }) { result -> when (result) { is SearchResult.Community -> { CommunityItem( diff --git a/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreViewModel.kt b/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreViewModel.kt index f706ba707..7aa09e8af 100644 --- a/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreViewModel.kt +++ b/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/ExploreViewModel.kt @@ -1,14 +1,14 @@ package com.github.diegoberaldin.raccoonforlemmy.unit.explore import cafe.adriel.voyager.core.model.screenModelScope +import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.ExplorePaginationManager +import com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination.ExplorePaginationSpecification import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent -import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository -import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DomainBlocklistRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository -import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.StopWordRepository +import com.github.diegoberaldin.raccoonforlemmy.core.utils.imagepreload.ImagePreloadManager import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository @@ -19,6 +19,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultType import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType +import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.imageUrl import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toListingType import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSearchResultType import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType @@ -27,7 +28,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.Communit import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.GetSortTypesUseCase import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.LemmyValueCache import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository -import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.UserRepository import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine @@ -45,7 +45,7 @@ class ExploreViewModel( private val apiConfigRepository: ApiConfigurationRepository, private val identityRepository: IdentityRepository, private val communityRepository: CommunityRepository, - private val userRepository: UserRepository, + private val paginationManager: ExplorePaginationManager, private val postRepository: PostRepository, private val commentRepository: CommentRepository, private val themeRepository: ThemeRepository, @@ -53,15 +53,12 @@ class ExploreViewModel( private val notificationCenter: NotificationCenter, private val hapticFeedback: HapticFeedback, private val getSortTypesUseCase: GetSortTypesUseCase, + private val imagePreloadManager: ImagePreloadManager, private val lemmyValueCache: LemmyValueCache, - private val accountRepository: AccountRepository, - private val domainBlocklistRepository: DomainBlocklistRepository, - private val stopWordRepository: StopWordRepository, ) : DefaultMviModel( initialState = ExploreMviModel.UiState(), ), ExploreMviModel { - private var currentPage: Int = 1 private val isOnOtherInstance: Boolean get() = otherInstance.isNotEmpty() private val notificationEventKey: String get() = @@ -72,8 +69,6 @@ class ExploreViewModel( append(otherInstance) } } - private var blockedDomains: List? = null - private var stopWords: List? = null init { screenModelScope.launch { @@ -362,16 +357,23 @@ class ExploreViewModel( } private suspend fun refresh(initial: Boolean = false) { - currentPage = 1 - val accountId = accountRepository.getActive()?.id - blockedDomains = domainBlocklistRepository.get(accountId) - stopWords = stopWordRepository.get(accountId) + paginationManager.reset( + ExplorePaginationSpecification( + listingType = uiState.value.listingType, + sortType = uiState.value.sortType, + query = uiState.value.searchText, + includeNsfw = settingsRepository.currentSettings.value.includeNsfw, + searchPostTitleOnly = settingsRepository.currentSettings.value.searchPostTitleOnly, + otherInstance = otherInstance, + resultType = uiState.value.resultType, + ), + ) updateState { it.copy( + initial = initial, canFetchMore = true, refreshing = !initial, loading = false, - initial = initial, ) } loadNextPage() @@ -384,161 +386,28 @@ class ExploreViewModel( return } updateState { it.copy(loading = true) } - val searchText = uiState.value.searchText - val auth = identityRepository.authToken.value - val refreshing = currentState.refreshing - val listingType = currentState.listingType - val sortType = currentState.sortType - val resultType = currentState.resultType - val settings = settingsRepository.currentSettings.value - val itemList = - communityRepository.search( - query = searchText, - auth = auth, - resultType = resultType, - page = currentPage, - listingType = listingType, - sortType = sortType, - instance = otherInstance, - ) - val additionalResolvedCommunity = - if (resultType == SearchResultType.All || - resultType == SearchResultType.Communities && - currentPage == 1 && - searchText.isNotEmpty() - ) { - communityRepository.getResolved( - query = searchText, - auth = auth, - ) - } else { - null + + val results = paginationManager.loadNextPage() + if (uiState.value.autoLoadImages) { + results.forEach { res -> + (res as? SearchResult.Post)?.model?.imageUrl?.takeIf { it.isNotEmpty() } + ?.also { url -> + imagePreloadManager.preload(url) + } } - val additionalResolvedUser = - if (resultType == SearchResultType.All || - resultType == SearchResultType.Users && - currentPage == 1 && - searchText.isNotEmpty() - ) { - userRepository.getResolved( - query = searchText, - auth = auth, - ) - } else { - null - } - if (itemList.isNotEmpty()) { - currentPage++ } - val itemsToAdd = - itemList - .filter { item -> - if (settings.includeNsfw) { - true - } else { - isSafeForWork(item) - } - }.filter { - when (it) { - is SearchResult.Post -> { - val filteredByDomain = - blockedDomains?.takeIf { l -> l.isNotEmpty() }?.let { blockList -> - blockList.none { domain -> - it.model.url?.contains(domain) ?: true - } - } ?: true - val filteredByStopWord = - stopWords?.takeIf { l -> l.isNotEmpty() }?.let { stopWordList -> - stopWordList.none { domain -> - it.model.title.contains(other = domain, ignoreCase = true) - } - } ?: true - filteredByDomain && filteredByStopWord - } - - else -> true - } - } - .let { - when (resultType) { - SearchResultType.Communities -> { - if (additionalResolvedCommunity != null && - it.none { r -> - r is SearchResult.Community && r.model.id == additionalResolvedCommunity.id - } - ) { - it + SearchResult.Community(additionalResolvedCommunity) - } else { - it - } - } - - SearchResultType.Users -> { - if (additionalResolvedUser != null && - it.none { r -> - r is SearchResult.User && r.model.id == additionalResolvedUser.id - } - ) { - it + SearchResult.User(additionalResolvedUser) - } else { - it - } - } - - SearchResultType.Posts -> { - if (settings.searchPostTitleOnly && searchText.isNotEmpty()) { - // apply the more restrictive title-only search - it - .filterIsInstance() - .filter { r -> - r.model.title.contains( - other = searchText, - ignoreCase = true, - ) - } - } else { - it - } - } - - else -> it - } - }.filter { item -> - if (refreshing) { - true - } else { - // prevents accidental duplication - currentState.results.none { other -> getItemKey(item) == getItemKey(other) } - } - } updateState { - val newItems = - if (refreshing) { - itemsToAdd - } else { - it.results + itemsToAdd - } it.copy( - results = newItems, + results = results, loading = false, - canFetchMore = itemList.isNotEmpty(), + canFetchMore = paginationManager.canFetchMore, refreshing = false, ) } } - private fun isSafeForWork(element: SearchResult): Boolean = - when (element) { - is SearchResult.Community -> !element.model.nsfw - is SearchResult.Post -> !element.model.nsfw - is SearchResult.Comment -> true - is SearchResult.User -> true - else -> false - } - private fun handleLogout() { screenModelScope.launch { - currentPage = 1 updateState { it.copy( listingType = ListingType.Local, @@ -914,11 +783,3 @@ class ExploreViewModel( } } } - -internal fun getItemKey(result: SearchResult): String = - when (result) { - is SearchResult.Post -> "post" + result.model.id.toString() + result.model.updateDate - is SearchResult.Comment -> "comment" + result.model.id.toString() + result.model.updateDate - is SearchResult.User -> "user" + result.model.id.toString() - is SearchResult.Community -> "community" + result.model.id.toString() - } diff --git a/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/di/ExploreModule.kt b/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/di/ExploreModule.kt index 51858aced..0d4ed54d4 100644 --- a/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/di/ExploreModule.kt +++ b/unit/explore/src/commonMain/kotlin/com/github/diegoberaldin/raccoonforlemmy/unit/explore/di/ExploreModule.kt @@ -11,9 +11,8 @@ val exploreModule = otherInstance = params[0], apiConfigRepository = get(), identityRepository = get(), - accountRepository = get(), + paginationManager = get(), communityRepository = get(), - userRepository = get(), postRepository = get(), commentRepository = get(), themeRepository = get(), @@ -22,8 +21,7 @@ val exploreModule = hapticFeedback = get(), getSortTypesUseCase = get(), lemmyValueCache = get(), - domainBlocklistRepository = get(), - stopWordRepository = get(), + imagePreloadManager = get(), ) } }