mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-03 11:37:37 +01:00
refactor: introduce pagination manager for Explore (#1185)
This commit is contained in:
parent
27c70317a4
commit
a987560626
@ -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()
|
||||
}
|
||||
|
@ -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<AccountRepository>(relaxUnitFun = true) {
|
||||
coEvery { getActive() } returns null
|
||||
}
|
||||
private val domainBlocklistRepository =
|
||||
mockk<DomainBlocklistRepository>(relaxUnitFun = true) {
|
||||
coEvery { get(accountId = any()) } returns emptyList()
|
||||
}
|
||||
private val stopWordRepository =
|
||||
mockk<StopWordRepository>(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<Int>()
|
||||
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<Int>()
|
||||
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"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -17,7 +17,6 @@ internal class DefaultCommunityPaginationManager(
|
||||
private val communityRepository: CommunityRepository,
|
||||
) : CommunityPaginationManager {
|
||||
override var canFetchMore: Boolean = true
|
||||
private set
|
||||
override val history: MutableList<CommunityModel> = mutableListOf()
|
||||
|
||||
private var specification: CommunityPaginationSpecification? = null
|
||||
|
@ -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<SearchResult> = mutableListOf()
|
||||
private var blockedDomains: List<String>? = null
|
||||
private var stopWords: List<String>? = null
|
||||
|
||||
override fun reset(specification: ExplorePaginationSpecification) {
|
||||
this.specification = specification
|
||||
canFetchMore = true
|
||||
currentPage = 1
|
||||
history.clear()
|
||||
blockedDomains = null
|
||||
stopWords = null
|
||||
}
|
||||
|
||||
override suspend fun loadNextPage(): List<SearchResult> =
|
||||
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<SearchResult> =
|
||||
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<SearchResult.Post>()
|
||||
.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<SearchResult>.deduplicate(): List<SearchResult> =
|
||||
filter { c1 ->
|
||||
// prevents accidental duplication
|
||||
history.none { c2 -> c2.uniqueIdentifier == c1.uniqueIdentifier }
|
||||
}
|
||||
|
||||
private fun List<SearchResult>.filterNsfw(includeNsfw: Boolean): List<SearchResult> =
|
||||
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<SearchResult>.filterDeleted(): List<SearchResult> {
|
||||
return filter {
|
||||
when (it) {
|
||||
is SearchResult.Post -> !it.model.deleted
|
||||
is SearchResult.Comment -> !it.model.deleted
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<SearchResult>.filterByUrlDomain(): List<SearchResult> =
|
||||
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<SearchResult>.filterByStopWords(): List<SearchResult> =
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -35,7 +35,6 @@ internal class DefaultPostPaginationManager(
|
||||
notificationCenter: NotificationCenter,
|
||||
) : PostPaginationManager {
|
||||
override var canFetchMore: Boolean = true
|
||||
private set
|
||||
override val history: MutableList<PostModel> = mutableListOf()
|
||||
|
||||
private var specification: PostPaginationSpecification? = null
|
||||
@ -301,16 +300,15 @@ internal class DefaultPostPaginationManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<PostModel>.filterByUrlDomain(): List<PostModel> {
|
||||
return filter { post ->
|
||||
private fun List<PostModel>.filterByUrlDomain(): List<PostModel> =
|
||||
filter { post ->
|
||||
blockedDomains?.takeIf { it.isNotEmpty() }?.let { blockList ->
|
||||
blockList.none { domain -> post.url?.contains(domain) ?: true }
|
||||
} ?: true
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<PostModel>.filterByStopWords(): List<PostModel> {
|
||||
return filter { post ->
|
||||
private fun List<PostModel>.filterByStopWords(): List<PostModel> =
|
||||
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
|
||||
|
@ -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<SearchResult>
|
||||
}
|
@ -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,
|
||||
)
|
@ -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<ExplorePaginationManager> {
|
||||
DefaultExplorePaginationManager(
|
||||
identityRepository = get(),
|
||||
accountRepository = get(),
|
||||
communityRepository = get(),
|
||||
userRepository = get(),
|
||||
domainBlocklistRepository = get(),
|
||||
stopWordRepository = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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<ExploreMviModel.Intent, ExploreMviModel.UiState, ExploreMviModel.Effect>(
|
||||
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<String>? = null
|
||||
private var stopWords: List<String>? = 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<SearchResult.Post>()
|
||||
.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()
|
||||
}
|
||||
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user