feat: URL based filter for posts (#1177)

This commit is contained in:
Diego Beraldin 2024-07-28 20:02:56 +02:00 committed by GitHub
parent 40d53b0f05
commit fed21d7fe7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 657 additions and 85 deletions

View File

@ -4,6 +4,7 @@ import androidx.compose.foundation.gestures.DraggableState
import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.draggable
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -13,11 +14,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
@Composable @Composable
fun SectionSelector( fun SectionSelector(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
draggable: Boolean = true, draggable: Boolean = true,
scrollable: Boolean = false,
titles: List<String> = emptyList(), titles: List<String> = emptyList(),
currentSection: Int, currentSection: Int,
onSectionSelected: (Int) -> Unit, onSectionSelected: (Int) -> Unit,
@ -29,14 +32,7 @@ fun SectionSelector(
isTowardsStart = delta > 0 isTowardsStart = delta > 0
} }
} }
TabRow( val draggableModifier =
modifier = modifier,
selectedTabIndex = currentSection,
tabs = {
titles.forEachIndexed { i, title ->
Tab(
modifier =
Modifier.then(
if (draggable) { if (draggable) {
Modifier.draggable( Modifier.draggable(
state = draggableState, state = draggableState,
@ -45,14 +41,49 @@ fun SectionSelector(
if (isTowardsStart) { if (isTowardsStart) {
onSectionSelected((currentSection - 1).coerceAtLeast(0)) onSectionSelected((currentSection - 1).coerceAtLeast(0))
} else { } else {
onSectionSelected((currentSection + 1).coerceAtMost(titles.lastIndex)) onSectionSelected(
(currentSection + 1).coerceAtMost(
titles.lastIndex,
),
)
} }
}, },
) )
} else { } else {
Modifier Modifier
}
if (scrollable) {
ScrollableTabRow(
modifier = modifier,
selectedTabIndex = currentSection,
edgePadding = Spacing.xs,
tabs = {
titles.forEachIndexed { i, title ->
Tab(
modifier = draggableModifier,
selected = i == currentSection,
text = {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onBackground,
)
}, },
), onClick = {
onSectionSelected(i)
},
)
}
},
)
} else {
TabRow(
modifier = modifier,
selectedTabIndex = currentSection,
tabs = {
titles.forEachIndexed { i, title ->
Tab(
modifier = draggableModifier,
selected = i == currentSection, selected = i == currentSection,
text = { text = {
Text( Text(
@ -69,3 +100,4 @@ fun SectionSelector(
}, },
) )
} }
}

View File

@ -436,4 +436,6 @@ internal open class DefaultStrings : Strings {
override val messageAuthIssueSegue1 = "force refresh" override val messageAuthIssueSegue1 = "force refresh"
override val messageAuthIssueSegue2 = "log in again" override val messageAuthIssueSegue2 = "log in again"
override val messageAuthIssueSegue3 = "clear the application data" override val messageAuthIssueSegue3 = "clear the application data"
override val settingsManageBanSectionDomains = "Domains"
override val settingsManageBanDomainPlaceholder = "Substring of URL to exclude"
} }

View File

@ -450,4 +450,6 @@ internal val ItStrings =
override val messageAuthIssueSegue1 = "forzare l\'aggiornamento" override val messageAuthIssueSegue1 = "forzare l\'aggiornamento"
override val messageAuthIssueSegue2 = "effettuare nuovamente l\'accesso" override val messageAuthIssueSegue2 = "effettuare nuovamente l\'accesso"
override val messageAuthIssueSegue3 = " cancellare i dati dell\'applicazione" override val messageAuthIssueSegue3 = " cancellare i dati dell\'applicazione"
override val settingsManageBanSectionDomains = "Domini"
override val settingsManageBanDomainPlaceholder = "Sottostringa URL da escludere"
} }

View File

@ -433,6 +433,8 @@ interface Strings {
val messageAuthIssueSegue1: String val messageAuthIssueSegue1: String
val messageAuthIssueSegue2: String val messageAuthIssueSegue2: String
val messageAuthIssueSegue3: String val messageAuthIssueSegue3: String
val settingsManageBanSectionDomains: String
val settingsManageBanDomainPlaceholder: String
} }
object Locales { object Locales {

View File

@ -29,7 +29,7 @@ class DefaultBottomNavItemsRepositoryTest {
assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res) assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res)
coVerify { coVerify {
keyStore.get("BottomNavItemsRepository.items", emptyList()) keyStore.get("$KEY_PREFIX.items", emptyList())
} }
} }
@ -42,7 +42,7 @@ class DefaultBottomNavItemsRepositoryTest {
assertEquals(ITEMS, res) assertEquals(ITEMS, res)
coVerify { coVerify {
keyStore.get("BottomNavItemsRepository.items", emptyList()) keyStore.get("$KEY_PREFIX.items", emptyList())
} }
} }
@ -56,7 +56,7 @@ class DefaultBottomNavItemsRepositoryTest {
assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res) assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res)
coVerify { coVerify {
keyStore.get("BottomNavItemsRepository.$accountId.items", emptyList()) keyStore.get("$KEY_PREFIX.$accountId.items", emptyList())
} }
} }
@ -70,7 +70,7 @@ class DefaultBottomNavItemsRepositoryTest {
assertEquals(ITEMS, res) assertEquals(ITEMS, res)
coVerify { coVerify {
keyStore.get("BottomNavItemsRepository.$accountId.items", emptyList()) keyStore.get("$KEY_PREFIX.$accountId.items", emptyList())
} }
} }
@ -81,13 +81,13 @@ class DefaultBottomNavItemsRepositoryTest {
val accountId = 2L val accountId = 2L
every { every {
keyStore.get( keyStore.get(
"BottomNavItemsRepository.$otherAccountId.items", "$KEY_PREFIX.$otherAccountId.items",
any<List<String>>(), any<List<String>>(),
) )
} returns ITEMS_IDS } returns ITEMS_IDS
every { every {
keyStore.get( keyStore.get(
"BottomNavItemsRepository.$accountId.items", "$KEY_PREFIX.$accountId.items",
any<List<String>>(), any<List<String>>(),
) )
} returns emptyList() } returns emptyList()
@ -96,7 +96,7 @@ class DefaultBottomNavItemsRepositoryTest {
assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res) assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res)
coVerify { coVerify {
keyStore.get("BottomNavItemsRepository.$accountId.items", emptyList()) keyStore.get("$KEY_PREFIX.$accountId.items", emptyList())
} }
} }
@ -106,13 +106,13 @@ class DefaultBottomNavItemsRepositoryTest {
val otherAccountId = 1 val otherAccountId = 1
every { every {
keyStore.get( keyStore.get(
"BottomNavItemsRepository.$otherAccountId.items", "$KEY_PREFIX.$otherAccountId.items",
any<List<String>>(), any<List<String>>(),
) )
} returns ITEMS_IDS } returns ITEMS_IDS
every { every {
keyStore.get( keyStore.get(
"BottomNavItemsRepository.items", "$KEY_PREFIX.items",
any<List<String>>(), any<List<String>>(),
) )
} returns emptyList() } returns emptyList()
@ -121,7 +121,7 @@ class DefaultBottomNavItemsRepositoryTest {
assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res) assertEquals(BottomNavItemsRepository.DEFAULT_ITEMS, res)
coVerify { coVerify {
keyStore.get("BottomNavItemsRepository.items", emptyList()) keyStore.get("$KEY_PREFIX.items", emptyList())
} }
} }
@ -131,7 +131,7 @@ class DefaultBottomNavItemsRepositoryTest {
sut.update(accountId = null, items = ITEMS) sut.update(accountId = null, items = ITEMS)
coVerify { coVerify {
keyStore.save("BottomNavItemsRepository.items", ITEMS_IDS) keyStore.save("$KEY_PREFIX.items", ITEMS_IDS)
} }
} }
@ -143,7 +143,7 @@ class DefaultBottomNavItemsRepositoryTest {
sut.update(accountId = accountId, items = ITEMS) sut.update(accountId = accountId, items = ITEMS)
coVerify { coVerify {
keyStore.save("BottomNavItemsRepository.1.items", ITEMS_IDS) keyStore.save("$KEY_PREFIX.$accountId.items", ITEMS_IDS)
} }
} }
@ -157,5 +157,6 @@ class DefaultBottomNavItemsRepositoryTest {
TabNavigationSection.Inbox, TabNavigationSection.Inbox,
TabNavigationSection.Settings, TabNavigationSection.Settings,
) )
private const val KEY_PREFIX = "BottomNavItemsRepository"
} }
} }

View File

@ -0,0 +1,159 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.core.testutils.DispatcherTestRule
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import kotlin.test.assertEquals
class DefaultDomainBlocklistRepositoryTest {
@get:Rule
val dispatcherRule = DispatcherTestRule()
private val keyStore = mockk<TemporaryKeyStore>(relaxUnitFun = true)
private val sut =
DefaultDomainBlocklistRepository(
keyStore = keyStore,
)
@Test
fun givenNoData_whenGetForAnonymousUser_thenResultAndInteractionsIsAsExpected() =
runTest {
every { keyStore.get(any(), any<List<String>>()) } returns emptyList()
val res = sut.get(null)
assertEquals(emptyList(), res)
coVerify {
keyStore.get("$KEY_PREFIX.items", emptyList())
}
}
@Test
fun givenData_whenGetForAnonymousUser_thenResultAndInteractionsIsAsExpected() =
runTest {
val fakeList = listOf("example.org")
every { keyStore.get(any(), any<List<String>>()) } returns fakeList
val res = sut.get(null)
assertEquals(fakeList, res)
coVerify {
keyStore.get("$KEY_PREFIX.items", emptyList())
}
}
@Test
fun givenNoData_whenGetForLoggedUser_thenResultAndInteractionsIsAsExpected() =
runTest {
val accountId = 1L
every { keyStore.get(any(), any<List<String>>()) } returns emptyList()
val res = sut.get(accountId)
assertEquals(emptyList(), res)
coVerify {
keyStore.get("$KEY_PREFIX.$accountId.items", emptyList())
}
}
@Test
fun givenData_whenGetForLoggedUser_thenResultAndInteractionsIsAsExpected() =
runTest {
val accountId = 1L
val fakeList = listOf("example.org")
every { keyStore.get(any(), any<List<String>>()) } returns fakeList
val res = sut.get(accountId)
assertEquals(fakeList, res)
coVerify {
keyStore.get("$KEY_PREFIX.$accountId.items", emptyList())
}
}
@Test
fun givenDataForOtherUser_whenGetForLoggedAccount_thenResultAndInteractionsIsAsExpected() =
runTest {
val otherAccountId = 1L
val accountId = 2L
val fakeList = listOf("example.org")
every {
keyStore.get(
"$KEY_PREFIX.$otherAccountId.items",
any<List<String>>(),
)
} returns fakeList
every {
keyStore.get(
"$KEY_PREFIX.$accountId.items",
any<List<String>>(),
)
} returns emptyList()
val res = sut.get(accountId)
assertEquals(emptyList(), res)
coVerify {
keyStore.get("$KEY_PREFIX.$accountId.items", emptyList())
}
}
@Test
fun givenDataForOtherUser_whenGetForAnonymousAccount_thenResultAndInteractionsIsAsExpected() =
runTest {
val otherAccountId = 1
val fakeList = listOf("example.org")
every {
keyStore.get(
"$KEY_PREFIX.$otherAccountId.items",
any<List<String>>(),
)
} returns fakeList
every {
keyStore.get(
"$KEY_PREFIX.items",
any<List<String>>(),
)
} returns emptyList()
val res = sut.get(null)
assertEquals(emptyList(), res)
coVerify {
keyStore.get("$KEY_PREFIX.items", emptyList())
}
}
@Test
fun whenUpdateAnonymousUser_thenInteractionsAreAsExpected() =
runTest {
val fakeList = listOf("example.org")
sut.update(accountId = null, items = fakeList)
coVerify {
keyStore.save("$KEY_PREFIX.items", fakeList)
}
}
@Test
fun whenUpdateLoggedUser_thenInteractionsAreAsExpected() =
runTest {
val accountId = 1L
val fakeList = listOf("example.org")
sut.update(accountId = accountId, items = fakeList)
coVerify {
keyStore.save("$KEY_PREFIX.$accountId.items", fakeList)
}
}
companion object {
private const val KEY_PREFIX = "DomainBlocklistRepository"
}
}

View File

@ -8,11 +8,13 @@ import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.Comm
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultAccountRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultAccountRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultCommunityPreferredLanguageRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultCommunityPreferredLanguageRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultCommunitySortRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultCommunitySortRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultDomainBlocklistRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultDraftRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultDraftRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultFavoriteCommunityRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultFavoriteCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultInstanceSelectionRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultInstanceSelectionRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultMultiCommunityRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultMultiCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultSettingsRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DomainBlocklistRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DraftRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DraftRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.FavoriteCommunityRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.FavoriteCommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.InstanceSelectionRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.InstanceSelectionRepository
@ -84,4 +86,9 @@ val corePersistenceModule =
settingsRepository = get(), settingsRepository = get(),
) )
} }
single<DomainBlocklistRepository> {
DefaultDomainBlocklistRepository(
keyStore = get(),
)
}
} }

View File

@ -0,0 +1,35 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
internal class DefaultDomainBlocklistRepository(
private val keyStore: TemporaryKeyStore,
) : DomainBlocklistRepository {
override suspend fun get(accountId: Long?): List<String> =
withContext(Dispatchers.IO) {
val key = getKey(accountId)
val res = keyStore.get(key, emptyList())
res.filter { it.isNotEmpty() }
}
override suspend fun update(
accountId: Long?,
items: List<String>,
) = withContext(Dispatchers.IO) {
val key = getKey(accountId)
keyStore.save(key, items)
}
private fun getKey(accountId: Long?): String =
buildString {
append("DomainBlocklistRepository")
if (accountId != null) {
append(".")
append(accountId)
}
append(".items")
}
}

View File

@ -0,0 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
interface DomainBlocklistRepository {
suspend fun get(accountId: Long?): List<String>
suspend fun update(
accountId: Long?,
items: List<String>,
)
}

View File

@ -40,6 +40,7 @@ kotlin {
implementation(projects.core.notifications) implementation(projects.core.notifications)
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.core.persistence)
implementation(projects.domain.identity) implementation(projects.domain.identity)
implementation(projects.domain.lemmy.data) implementation(projects.domain.lemmy.data)

View File

@ -2,6 +2,8 @@ package com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent 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.testutils.DispatcherTestRule import com.github.diegoberaldin.raccoonforlemmy.core.testutils.DispatcherTestRule
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository 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.PostModel
@ -45,16 +47,26 @@ class DefaultPostPaginationManagerTest {
val slot = slot<KClass<NotificationCenterEvent>>() val slot = slot<KClass<NotificationCenterEvent>>()
every { subscribe(capture(slot)) } answers { MutableSharedFlow() } every { subscribe(capture(slot)) } answers { MutableSharedFlow() }
} }
private val accountRepository =
mockk<AccountRepository>(relaxUnitFun = true) {
coEvery { getActive() } returns null
}
private val domainBlocklistRepository =
mockk<DomainBlocklistRepository>(relaxUnitFun = true) {
coEvery { get(accountId = any()) } returns emptyList<String>()
}
private val sut = private val sut =
DefaultPostPaginationManager( DefaultPostPaginationManager(
identityRepository = identityRepository, identityRepository = identityRepository,
accountRepository = accountRepository,
postRepository = postRepository, postRepository = postRepository,
communityRepository = communityRepository, communityRepository = communityRepository,
userRepository = userRepository, userRepository = userRepository,
multiCommunityPaginator = multiCommunityPaginator, multiCommunityPaginator = multiCommunityPaginator,
notificationCenter = notificationCenter, notificationCenter = notificationCenter,
dispatcher = dispatcherTestRule.dispatcher, dispatcher = dispatcherTestRule.dispatcher,
domainBlocklistRepository = domainBlocklistRepository,
) )
@Test @Test

View File

@ -2,6 +2,8 @@ package com.diegoberaldin.raccoonforlemmy.domain.lemmy.pagination
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent 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.domain.identity.repository.IdentityRepository 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.PostModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResult
@ -21,10 +23,12 @@ import kotlinx.coroutines.withContext
internal class DefaultPostPaginationManager( internal class DefaultPostPaginationManager(
private val identityRepository: IdentityRepository, private val identityRepository: IdentityRepository,
private val accountRepository: AccountRepository,
private val postRepository: PostRepository, private val postRepository: PostRepository,
private val communityRepository: CommunityRepository, private val communityRepository: CommunityRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val multiCommunityPaginator: MultiCommunityPaginator, private val multiCommunityPaginator: MultiCommunityPaginator,
private val domainBlocklistRepository: DomainBlocklistRepository,
dispatcher: CoroutineDispatcher = Dispatchers.IO, dispatcher: CoroutineDispatcher = Dispatchers.IO,
notificationCenter: NotificationCenter, notificationCenter: NotificationCenter,
) : PostPaginationManager { ) : PostPaginationManager {
@ -36,6 +40,7 @@ internal class DefaultPostPaginationManager(
private var currentPage: Int = 1 private var currentPage: Int = 1
private var pageCursor: String? = null private var pageCursor: String? = null
private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher) private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
private var blockedDomains: List<String>? = null
init { init {
notificationCenter notificationCenter
@ -52,6 +57,7 @@ internal class DefaultPostPaginationManager(
currentPage = 1 currentPage = 1
pageCursor = null pageCursor = null
multiCommunityPaginator.reset() multiCommunityPaginator.reset()
blockedDomains = null
(specification as? PostPaginationSpecification.MultiCommunity)?.also { (specification as? PostPaginationSpecification.MultiCommunity)?.also {
multiCommunityPaginator.setCommunities(it.communityIds) multiCommunityPaginator.setCommunities(it.communityIds)
} }
@ -61,6 +67,10 @@ internal class DefaultPostPaginationManager(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val specification = specification ?: return@withContext emptyList() val specification = specification ?: return@withContext emptyList()
val auth = identityRepository.authToken.value.orEmpty() val auth = identityRepository.authToken.value.orEmpty()
if (blockedDomains == null) {
val accountId = accountRepository.getActive()?.id
blockedDomains = domainBlocklistRepository.get(accountId)
}
val result = val result =
when (specification) { when (specification) {
@ -85,6 +95,7 @@ internal class DefaultPostPaginationManager(
.deduplicate() .deduplicate()
.filterNsfw(specification.includeNsfw) .filterNsfw(specification.includeNsfw)
.filterDeleted() .filterDeleted()
.filterByUrlDomain()
} }
is PostPaginationSpecification.Community -> { is PostPaginationSpecification.Community -> {
@ -125,6 +136,7 @@ internal class DefaultPostPaginationManager(
.deduplicate() .deduplicate()
.filterNsfw(specification.includeNsfw) .filterNsfw(specification.includeNsfw)
.filterDeleted(includeCurrentCreator = true) .filterDeleted(includeCurrentCreator = true)
.filterByUrlDomain()
} }
is PostPaginationSpecification.MultiCommunity -> { is PostPaginationSpecification.MultiCommunity -> {
@ -138,6 +150,7 @@ internal class DefaultPostPaginationManager(
.deduplicate() .deduplicate()
.filterNsfw(specification.includeNsfw) .filterNsfw(specification.includeNsfw)
.filterDeleted(includeCurrentCreator = true) .filterDeleted(includeCurrentCreator = true)
.filterByUrlDomain()
} }
is PostPaginationSpecification.User -> { is PostPaginationSpecification.User -> {
@ -159,6 +172,7 @@ internal class DefaultPostPaginationManager(
.deduplicate() .deduplicate()
.filterNsfw(specification.includeNsfw) .filterNsfw(specification.includeNsfw)
.filterDeleted(includeCurrentCreator = specification.includeDeleted) .filterDeleted(includeCurrentCreator = specification.includeDeleted)
.filterByUrlDomain()
} }
is PostPaginationSpecification.Votes -> { is PostPaginationSpecification.Votes -> {
@ -181,6 +195,7 @@ internal class DefaultPostPaginationManager(
.orEmpty() .orEmpty()
.deduplicate() .deduplicate()
.filterDeleted(includeCurrentCreator = true) .filterDeleted(includeCurrentCreator = true)
.filterByUrlDomain()
} }
is PostPaginationSpecification.Saved -> { is PostPaginationSpecification.Saved -> {
@ -199,6 +214,7 @@ internal class DefaultPostPaginationManager(
.orEmpty() .orEmpty()
.deduplicate() .deduplicate()
.filterDeleted() .filterDeleted()
.filterByUrlDomain()
} }
is PostPaginationSpecification.Hidden -> { is PostPaginationSpecification.Hidden -> {
@ -220,6 +236,7 @@ internal class DefaultPostPaginationManager(
.orEmpty() .orEmpty()
.deduplicate() .deduplicate()
.filterDeleted() .filterDeleted()
.filterByUrlDomain()
} }
} }
@ -233,6 +250,7 @@ internal class DefaultPostPaginationManager(
specification = specification, specification = specification,
currentPage = currentPage, currentPage = currentPage,
pageCursor = pageCursor, pageCursor = pageCursor,
blockedDomains = blockedDomains,
history = history, history = history,
) )
@ -242,6 +260,7 @@ internal class DefaultPostPaginationManager(
specification = it.specification specification = it.specification
pageCursor = it.pageCursor pageCursor = it.pageCursor
history.clear() history.clear()
blockedDomains = it.blockedDomains
history.addAll(it.history) history.addAll(it.history)
} }
} }
@ -266,6 +285,14 @@ internal class DefaultPostPaginationManager(
} }
} }
private fun List<PostModel>.filterByUrlDomain(): List<PostModel> {
return filter { post ->
blockedDomains?.takeIf { it.isNotEmpty() }?.let { blockList ->
blockList.none { domain -> post.url?.contains(domain) ?: true }
} ?: true
}
}
private fun handlePostUpdate(post: PostModel) { private fun handlePostUpdate(post: PostModel) {
val index = history.indexOfFirst { it.id == post.id }.takeIf { it >= 0 } ?: return val index = history.indexOfFirst { it.id == post.id }.takeIf { it >= 0 } ?: return
history.removeAt(index) history.removeAt(index)

View File

@ -7,4 +7,5 @@ internal data class DefaultPostPaginationManagerState(
val currentPage: Int = 1, val currentPage: Int = 1,
val pageCursor: String? = null, val pageCursor: String? = null,
val history: List<PostModel> = emptyList(), val history: List<PostModel> = emptyList(),
val blockedDomains: List<String>? = null,
) : PostPaginationManagerState ) : PostPaginationManagerState

View File

@ -22,11 +22,13 @@ val paginationModule =
factory<PostPaginationManager> { factory<PostPaginationManager> {
DefaultPostPaginationManager( DefaultPostPaginationManager(
identityRepository = get(), identityRepository = get(),
accountRepository = get(),
postRepository = get(), postRepository = get(),
communityRepository = get(), communityRepository = get(),
userRepository = get(), userRepository = get(),
multiCommunityPaginator = get(), multiCommunityPaginator = get(),
notificationCenter = get(), notificationCenter = get(),
domainBlocklistRepository = get(),
) )
} }
factory<CommentPaginationManager> { factory<CommentPaginationManager> {

View File

@ -5,6 +5,8 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.Theme
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterEvent 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.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.vibrate.HapticFeedback 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.ApiConfigurationRepository
@ -51,6 +53,8 @@ class ExploreViewModel(
private val hapticFeedback: HapticFeedback, private val hapticFeedback: HapticFeedback,
private val getSortTypesUseCase: GetSortTypesUseCase, private val getSortTypesUseCase: GetSortTypesUseCase,
private val lemmyValueCache: LemmyValueCache, private val lemmyValueCache: LemmyValueCache,
private val accountRepository: AccountRepository,
private val domainBlocklistRepository: DomainBlocklistRepository,
) : DefaultMviModel<ExploreMviModel.Intent, ExploreMviModel.UiState, ExploreMviModel.Effect>( ) : DefaultMviModel<ExploreMviModel.Intent, ExploreMviModel.UiState, ExploreMviModel.Effect>(
initialState = ExploreMviModel.UiState(), initialState = ExploreMviModel.UiState(),
), ),
@ -66,6 +70,7 @@ class ExploreViewModel(
append(otherInstance) append(otherInstance)
} }
} }
private var blockedDomains: List<String>? = null
init { init {
screenModelScope.launch { screenModelScope.launch {
@ -355,6 +360,8 @@ class ExploreViewModel(
private suspend fun refresh(initial: Boolean = false) { private suspend fun refresh(initial: Boolean = false) {
currentPage = 1 currentPage = 1
val accountId = accountRepository.getActive()?.id
blockedDomains = domainBlocklistRepository.get(accountId)
updateState { updateState {
it.copy( it.copy(
canFetchMore = true, canFetchMore = true,
@ -427,7 +434,18 @@ class ExploreViewModel(
} else { } else {
isSafeForWork(item) isSafeForWork(item)
} }
}.let { }.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
}
}
.let {
when (resultType) { when (resultType) {
SearchResultType.Communities -> { SearchResultType.Communities -> {
if (additionalResolvedCommunity != null && if (additionalResolvedCommunity != null &&

View File

@ -11,6 +11,7 @@ val exploreModule =
otherInstance = params[0], otherInstance = params[0],
apiConfigRepository = get(), apiConfigRepository = get(),
identityRepository = get(), identityRepository = get(),
accountRepository = get(),
communityRepository = get(), communityRepository = get(),
userRepository = get(), userRepository = get(),
postRepository = get(), postRepository = get(),
@ -21,6 +22,7 @@ val exploreModule =
hapticFeedback = get(), hapticFeedback = get(),
getSortTypesUseCase = get(), getSortTypesUseCase = get(),
lemmyValueCache = get(), lemmyValueCache = get(),
domainBlocklistRepository = get(),
) )
} }
} }

View File

@ -7,12 +7,6 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.InstanceModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.InstanceModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
enum class ManageBanSection {
Users,
Communities,
Instances,
}
@Stable @Stable
interface ManageBanMviModel : interface ManageBanMviModel :
MviModel<ManageBanMviModel.Intent, ManageBanMviModel.UiState, ManageBanMviModel.Effect>, MviModel<ManageBanMviModel.Intent, ManageBanMviModel.UiState, ManageBanMviModel.Effect>,
@ -39,6 +33,14 @@ interface ManageBanMviModel :
data class SetSearch( data class SetSearch(
val value: String, val value: String,
) : Intent ) : Intent
data class BlockDomain(
val value: String,
) : Intent
data class UnblockDomain(
val value: String,
) : Intent
} }
data class UiState( data class UiState(
@ -50,6 +52,7 @@ interface ManageBanMviModel :
val bannedUsers: List<UserModel> = emptyList(), val bannedUsers: List<UserModel> = emptyList(),
val bannedCommunities: List<CommunityModel> = emptyList(), val bannedCommunities: List<CommunityModel> = emptyList(),
val bannedInstances: List<InstanceModel> = emptyList(), val bannedInstances: List<InstanceModel> = emptyList(),
val blockedDomains: List<String> = emptyList(),
val searchText: String = "", val searchText: String = "",
) )

View File

@ -1,5 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.manageban package com.github.diegoberaldin.raccoonforlemmy.unit.manageban
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@ -7,6 +10,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@ -14,6 +18,8 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.AddCircle
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
@ -31,7 +37,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
@ -44,6 +53,8 @@ import androidx.compose.ui.text.style.TextAlign
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.koin.getScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenu
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.FloatingActionButtonMenuItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SearchField import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SearchField
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SectionSelector
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CommunityItem import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CommunityItem
@ -51,15 +62,19 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.CommunityI
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.Option import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.Option
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.UserItem import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.UserItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.di.getFabNestedScrollConnection
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.EditTextualInfoDialog
import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings import com.github.diegoberaldin.raccoonforlemmy.core.l10n.messages.LocalStrings
import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator import com.github.diegoberaldin.raccoonforlemmy.core.navigation.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository import com.github.diegoberaldin.raccoonforlemmy.core.persistence.di.getSettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallback
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.rememberCallbackArgs
import com.github.diegoberaldin.raccoonforlemmy.unit.manageban.components.InstanceItem import com.github.diegoberaldin.raccoonforlemmy.unit.manageban.components.ManageBanItem
import com.github.diegoberaldin.raccoonforlemmy.unit.manageban.components.ManageBanItemPlaceholder
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
class ManageBanScreen : Screen { class ManageBanScreen : Screen {
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@ -70,6 +85,8 @@ class ManageBanScreen : Screen {
val navigationCoordinator = remember { getNavigationCoordinator() } val navigationCoordinator = remember { getNavigationCoordinator() }
val topAppBarState = rememberTopAppBarState() val topAppBarState = rememberTopAppBarState()
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState) val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
val fabNestedScrollConnection = remember { getFabNestedScrollConnection() }
val isFabVisible by fabNestedScrollConnection.isFabVisible.collectAsState()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val settingsRepository = remember { getSettingsRepository() } val settingsRepository = remember { getSettingsRepository() }
val settings by settingsRepository.currentSettings.collectAsState() val settings by settingsRepository.currentSettings.collectAsState()
@ -89,6 +106,8 @@ class ManageBanScreen : Screen {
} }
val successMessage = LocalStrings.current.messageOperationSuccessful val successMessage = LocalStrings.current.messageOperationSuccessful
val errorMessage = LocalStrings.current.messageGenericError val errorMessage = LocalStrings.current.messageGenericError
val scope = rememberCoroutineScope()
var addDomainDialogOpen by remember { mutableStateOf(false) }
LaunchedEffect(model) { LaunchedEffect(model) {
model.effects model.effects
@ -151,6 +170,51 @@ class ManageBanScreen : Screen {
) )
} }
}, },
floatingActionButton = {
AnimatedVisibility(
visible = isFabVisible,
enter =
slideInVertically(
initialOffsetY = { it * 2 },
),
exit =
slideOutVertically(
targetOffsetY = { it * 2 },
),
) {
FloatingActionButtonMenu(
items =
buildList {
this +=
FloatingActionButtonMenuItem(
icon = Icons.Default.ExpandLess,
text = LocalStrings.current.actionBackToTop,
onSelected =
rememberCallback {
scope.launch {
runCatching {
lazyListState.scrollToItem(0)
topAppBarState.heightOffset = 0f
topAppBarState.contentOffset = 0f
}
}
},
)
if (uiState.section == ManageBanSection.Domains) {
this +=
FloatingActionButtonMenuItem(
icon = Icons.Default.AddCircle,
text = LocalStrings.current.buttonAdd,
onSelected =
rememberCallback {
addDomainDialogOpen = true
},
)
}
},
)
}
},
) { padding -> ) { padding ->
val pullRefreshState = val pullRefreshState =
rememberPullRefreshState( rememberPullRefreshState(
@ -165,7 +229,8 @@ class ManageBanScreen : Screen {
Modifier Modifier
.padding( .padding(
top = padding.calculateTopPadding(), top = padding.calculateTopPadding(),
).then( ).navigationBarsPadding()
.then(
if (settings.hideNavigationBarWhileScrolling) { if (settings.hideNavigationBarWhileScrolling) {
Modifier.nestedScroll(scrollBehavior.nestedScrollConnection) Modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
} else { } else {
@ -198,20 +263,12 @@ class ManageBanScreen : Screen {
LocalStrings.current.exploreResultTypeUsers, LocalStrings.current.exploreResultTypeUsers,
LocalStrings.current.exploreResultTypeCommunities, LocalStrings.current.exploreResultTypeCommunities,
LocalStrings.current.settingsManageBanSectionInstances, LocalStrings.current.settingsManageBanSectionInstances,
LocalStrings.current.settingsManageBanSectionDomains,
), ),
currentSection = scrollable = true,
when (uiState.section) { currentSection = uiState.section.toInt(),
ManageBanSection.Instances -> 2 onSectionSelected = { idx ->
ManageBanSection.Communities -> 1 val section = idx.toManageBanSection()
else -> 0
},
onSectionSelected = {
val section =
when (it) {
2 -> ManageBanSection.Instances
1 -> ManageBanSection.Communities
else -> ManageBanSection.Users
}
model.reduce(ManageBanMviModel.Intent.ChangeSection(section)) model.reduce(ManageBanMviModel.Intent.ChangeSection(section))
}, },
) )
@ -255,7 +312,10 @@ class ManageBanScreen : Screen {
} }
} }
} else { } else {
items(uiState.bannedUsers) { user -> items(
items = uiState.bannedUsers,
key = { it.id },
) { user ->
UserItem( UserItem(
user = user, user = user,
autoLoadImages = uiState.autoLoadImages, autoLoadImages = uiState.autoLoadImages,
@ -308,7 +368,10 @@ class ManageBanScreen : Screen {
} }
} }
} else { } else {
items(uiState.bannedCommunities) { community -> items(
items = uiState.bannedCommunities,
key = { it.id },
) { community ->
CommunityItem( CommunityItem(
community = community, community = community,
autoLoadImages = uiState.autoLoadImages, autoLoadImages = uiState.autoLoadImages,
@ -344,7 +407,7 @@ class ManageBanScreen : Screen {
if (uiState.bannedInstances.isEmpty()) { if (uiState.bannedInstances.isEmpty()) {
if (uiState.initial) { if (uiState.initial) {
items(5) { items(5) {
CommunityItemPlaceholder() ManageBanItemPlaceholder()
} }
} else { } else {
item { item {
@ -361,9 +424,12 @@ class ManageBanScreen : Screen {
} }
} }
} else { } else {
items(uiState.bannedInstances) { instance -> items(
InstanceItem( items = uiState.bannedInstances,
instance = instance, key = { it.id },
) { instance ->
ManageBanItem(
title = instance.domain,
options = options =
buildList { buildList {
this += this +=
@ -390,6 +456,60 @@ class ManageBanScreen : Screen {
} }
} }
} }
ManageBanSection.Domains -> {
if (uiState.blockedDomains.isEmpty()) {
if (uiState.initial) {
items(5) {
ManageBanItemPlaceholder()
}
} else {
item {
Text(
modifier =
Modifier
.fillMaxWidth()
.padding(top = Spacing.xs),
textAlign = TextAlign.Center,
text = LocalStrings.current.messageEmptyList,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onBackground,
)
}
}
} else {
items(
items = uiState.blockedDomains,
key = { it },
) { domain ->
ManageBanItem(
title = domain,
options =
buildList {
this +=
Option(
OptionId.Unban,
LocalStrings.current.settingsManageBanActionUnban,
)
},
onOptionSelected =
rememberCallbackArgs(domain) { optionId ->
when (optionId) {
OptionId.Unban -> {
model.reduce(
ManageBanMviModel.Intent.UnblockDomain(
domain,
),
)
}
else -> Unit
}
},
)
}
}
}
} }
} }
@ -403,5 +523,19 @@ class ManageBanScreen : Screen {
} }
} }
} }
if (addDomainDialogOpen) {
EditTextualInfoDialog(
title = LocalStrings.current.settingsManageBanDomainPlaceholder,
value = "",
onClose =
rememberCallbackArgs(model) { newValue ->
addDomainDialogOpen = false
newValue?.also {
model.reduce(ManageBanMviModel.Intent.BlockDomain(it))
}
},
)
}
} }
} }

View File

@ -0,0 +1,27 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.manageban
sealed interface ManageBanSection {
data object Users : ManageBanSection
data object Communities : ManageBanSection
data object Instances : ManageBanSection
data object Domains : ManageBanSection
}
fun ManageBanSection.toInt(): Int =
when (this) {
ManageBanSection.Communities -> 1
ManageBanSection.Domains -> 3
ManageBanSection.Instances -> 2
ManageBanSection.Users -> 0
}
fun Int.toManageBanSection(): ManageBanSection =
when (this) {
3 -> ManageBanSection.Domains
2 -> ManageBanSection.Instances
1 -> ManageBanSection.Communities
else -> ManageBanSection.Users
}

View File

@ -2,6 +2,8 @@ package com.github.diegoberaldin.raccoonforlemmy.unit.manageban
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
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.SettingsRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.AccountBansModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.AccountBansModel
@ -25,15 +27,18 @@ import kotlinx.coroutines.launch
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class ManageBanViewModel( class ManageBanViewModel(
private val identityRepository: IdentityRepository, private val identityRepository: IdentityRepository,
private val accountRepository: AccountRepository,
private val siteRepository: SiteRepository, private val siteRepository: SiteRepository,
private val settingsRepository: SettingsRepository, private val settingsRepository: SettingsRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val communityRepository: CommunityRepository, private val communityRepository: CommunityRepository,
private val blocklistRepository: DomainBlocklistRepository,
) : DefaultMviModel<ManageBanMviModel.Intent, ManageBanMviModel.UiState, ManageBanMviModel.Effect>( ) : DefaultMviModel<ManageBanMviModel.Intent, ManageBanMviModel.UiState, ManageBanMviModel.Effect>(
initialState = ManageBanMviModel.UiState(), initialState = ManageBanMviModel.UiState(),
), ),
ManageBanMviModel { ManageBanMviModel {
private var originalBans: AccountBansModel? = null private var originalBans: AccountBansModel? = null
private var originalBlockedDomains: List<String> = emptyList()
init { init {
screenModelScope.launch { screenModelScope.launch {
@ -83,6 +88,8 @@ class ManageBanViewModel(
is ManageBanMviModel.Intent.UnblockCommunity -> unbanCommunity(intent.id) is ManageBanMviModel.Intent.UnblockCommunity -> unbanCommunity(intent.id)
is ManageBanMviModel.Intent.UnblockInstance -> unbanInstance(intent.id) is ManageBanMviModel.Intent.UnblockInstance -> unbanInstance(intent.id)
is ManageBanMviModel.Intent.UnblockUser -> unbanUser(intent.id) is ManageBanMviModel.Intent.UnblockUser -> unbanUser(intent.id)
is ManageBanMviModel.Intent.BlockDomain -> blockDomain(intent.value)
is ManageBanMviModel.Intent.UnblockDomain -> unblockDomain(intent.value)
is ManageBanMviModel.Intent.SetSearch -> updateSearchText(intent.value) is ManageBanMviModel.Intent.SetSearch -> updateSearchText(intent.value)
} }
} }
@ -90,6 +97,8 @@ class ManageBanViewModel(
private suspend fun refresh() { private suspend fun refresh() {
val auth = identityRepository.authToken.value.orEmpty() val auth = identityRepository.authToken.value.orEmpty()
originalBans = siteRepository.getBans(auth) originalBans = siteRepository.getBans(auth)
val accountId = accountRepository.getActive()?.id
originalBlockedDomains = blocklistRepository.get(accountId)
val query = uiState.value.searchText val query = uiState.value.searchText
filterResults(query) filterResults(query)
} }
@ -164,10 +173,38 @@ class ManageBanViewModel(
bannedUsers = bans.users.filterUsersBy(query), bannedUsers = bans.users.filterUsersBy(query),
bannedCommunities = bans.communities.filterCommunitiesBy(query), bannedCommunities = bans.communities.filterCommunitiesBy(query),
bannedInstances = bans.instances.filterInstancesBy(query), bannedInstances = bans.instances.filterInstancesBy(query),
blockedDomains = originalBlockedDomains.filterBy(query),
initial = false, initial = false,
) )
} }
} }
private fun blockDomain(domain: String) {
val newValues =
if (originalBlockedDomains.contains(domain)) {
originalBlockedDomains
} else {
originalBlockedDomains + domain
}
screenModelScope.launch {
val accountId = accountRepository.getActive()?.id
originalBlockedDomains = newValues
blocklistRepository.update(accountId, newValues)
val query = uiState.value.searchText
filterResults(query)
}
}
private fun unblockDomain(domain: String) {
val newValues = originalBlockedDomains - domain
screenModelScope.launch {
val accountId = accountRepository.getActive()?.id
originalBlockedDomains = newValues
blocklistRepository.update(accountId, newValues)
val query = uiState.value.searchText
filterResults(query)
}
}
} }
private fun List<UserModel>.filterUsersBy(query: String): List<UserModel> = private fun List<UserModel>.filterUsersBy(query: String): List<UserModel> =
@ -188,5 +225,12 @@ private fun List<InstanceModel>.filterInstancesBy(query: String): List<InstanceM
if (query.isEmpty()) { if (query.isEmpty()) {
this this
} else { } else {
filter { it.domain.contains(query) } filter { it.domain.contains(query, ignoreCase = true) }
}
private fun List<String>.filterBy(query: String): List<String> =
if (query.isEmpty()) {
this
} else {
filter { it.contains(query, ignoreCase = true) }
} }

View File

@ -32,16 +32,14 @@ import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.Option
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId import com.github.diegoberaldin.raccoonforlemmy.core.commonui.lemmyui.OptionId
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.onClick
import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp import com.github.diegoberaldin.raccoonforlemmy.core.utils.toLocalDp
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.InstanceModel
@Composable @Composable
fun InstanceItem( internal fun ManageBanItem(
instance: InstanceModel, title: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
options: List<Option> = emptyList(), options: List<Option> = emptyList(),
onOptionSelected: ((OptionId) -> Unit)? = null, onOptionSelected: ((OptionId) -> Unit)? = null,
) { ) {
val name = instance.domain
val iconSize = 30.dp val iconSize = 30.dp
val fullColor = MaterialTheme.colorScheme.onBackground val fullColor = MaterialTheme.colorScheme.onBackground
val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ancillaryTextAlpha) val ancillaryColor = MaterialTheme.colorScheme.onBackground.copy(alpha = ancillaryTextAlpha)
@ -59,12 +57,12 @@ fun InstanceItem(
) { ) {
PlaceholderImage( PlaceholderImage(
size = iconSize, size = iconSize,
title = name, title = title,
) )
Text( Text(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
text = name, text = title,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = fullColor, color = fullColor,
) )

View File

@ -0,0 +1,51 @@
package com.github.diegoberaldin.raccoonforlemmy.unit.manageban.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.CornerSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.IconSize
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.utils.compose.shimmerEffect
@Composable
internal fun ManageBanItemPlaceholder() {
Row(
modifier =
Modifier.padding(
vertical = Spacing.xs,
horizontal = Spacing.s,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
Box(
modifier =
Modifier
.padding(Spacing.xxxs)
.size(IconSize.l)
.clip(CircleShape)
.shimmerEffect(),
)
Box(
modifier =
Modifier
.padding(start = Spacing.xs)
.height(40.dp)
.fillMaxWidth()
.clip(RoundedCornerShape(CornerSize.s))
.shimmerEffect(),
)
}
}

View File

@ -9,10 +9,12 @@ val manageBanModule =
factory<ManageBanMviModel> { factory<ManageBanMviModel> {
ManageBanViewModel( ManageBanViewModel(
identityRepository = get(), identityRepository = get(),
accountRepository = get(),
siteRepository = get(), siteRepository = get(),
settingsRepository = get(), settingsRepository = get(),
userRepository = get(), userRepository = get(),
communityRepository = get(), communityRepository = get(),
blocklistRepository = get(),
) )
} }
} }