mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-08 19:18:47 +01:00
fix: pagination in subscription list (#911)
This commit is contained in:
parent
2b7a885904
commit
bb507624fa
@ -193,34 +193,49 @@ class DefaultCommunityRepositoryTest {
|
||||
fun givenSuccess_whenGetSubscribed_thenResultIsAsExpected() =
|
||||
runTest {
|
||||
coEvery {
|
||||
siteService.get(
|
||||
searchService.search(
|
||||
authHeader = any(),
|
||||
auth = any(),
|
||||
q = any(),
|
||||
communityId = any(),
|
||||
communityName = any(),
|
||||
creatorId = any(),
|
||||
type = any(),
|
||||
sort = any(),
|
||||
listingType = any(),
|
||||
page = any(),
|
||||
limit = any(),
|
||||
)
|
||||
} returns
|
||||
mockk {
|
||||
every { myUser } returns
|
||||
mockk {
|
||||
every { follows } returns
|
||||
listOf(
|
||||
mockk {
|
||||
every { community } returns mockk(relaxed = true)
|
||||
},
|
||||
)
|
||||
}
|
||||
every { communities } returns listOf(
|
||||
mockk(relaxed = true) {
|
||||
every { community } returns mockk(relaxed = true)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
val token = "fake-token"
|
||||
val res =
|
||||
sut.getSubscribed(
|
||||
auth = token,
|
||||
page = 1,
|
||||
)
|
||||
|
||||
assertEquals(1, res.size)
|
||||
coVerify {
|
||||
siteService.get(
|
||||
auth = token,
|
||||
searchService.search(
|
||||
authHeader = token.toAuthHeader(),
|
||||
auth = token,
|
||||
q = "",
|
||||
communityId = null,
|
||||
communityName = null,
|
||||
creatorId = null,
|
||||
type = SearchType.Communities,
|
||||
sort = null,
|
||||
listingType = ListingType.Subscribed.toDto(),
|
||||
page = 1,
|
||||
limit = 20,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,12 @@ interface CommunityRepository {
|
||||
auth: String? = null,
|
||||
): CommunityModel?
|
||||
|
||||
suspend fun getSubscribed(auth: String? = null): List<CommunityModel>
|
||||
suspend fun getSubscribed(
|
||||
auth: String? = null,
|
||||
page: Int,
|
||||
limit: Int = DEFAULT_PAGE_SIZE,
|
||||
query: String = "",
|
||||
): List<CommunityModel>
|
||||
|
||||
suspend fun get(
|
||||
auth: String? = null,
|
||||
|
@ -117,15 +117,25 @@ internal class DefaultCommunityRepository(
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override suspend fun getSubscribed(auth: String?): List<CommunityModel> =
|
||||
override suspend fun getSubscribed(
|
||||
auth: String?,
|
||||
page: Int,
|
||||
limit: Int,
|
||||
query: String,
|
||||
): List<CommunityModel> =
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val response =
|
||||
services.site.get(
|
||||
services.search.search(
|
||||
authHeader = auth.toAuthHeader(),
|
||||
q = query,
|
||||
auth = auth,
|
||||
page = page,
|
||||
limit = limit,
|
||||
type = SearchResultType.Communities.toDto(),
|
||||
listingType = ListingType.Subscribed.toDto(),
|
||||
)
|
||||
response.myUser?.follows?.map { it.community.toModel() }.orEmpty()
|
||||
response.communities.map { it.toModel() }
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@ -15,6 +16,7 @@ import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -39,6 +41,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import cafe.adriel.voyager.navigator.tab.Tab
|
||||
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||
@ -274,6 +277,23 @@ object ModalDrawerContent : Tab {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||
model.reduce(ModalDrawerMviModel.Intent.LoadNextPage)
|
||||
}
|
||||
if (uiState.loading && !uiState.refreshing) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(25.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
PullRefreshIndicator(
|
||||
refreshing = uiState.refreshing,
|
||||
|
@ -15,9 +15,12 @@ interface ModalDrawerMviModel :
|
||||
data object Refresh : Intent
|
||||
|
||||
data class SetSearch(val value: String) : Intent
|
||||
data object LoadNextPage : Intent
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val loading: Boolean = false,
|
||||
val canFetchMore: Boolean = true,
|
||||
val user: UserModel? = null,
|
||||
val autoLoadImages: Boolean = true,
|
||||
val preferNicknames: Boolean = true,
|
||||
|
@ -43,6 +43,7 @@ class ModalDrawerViewModel(
|
||||
DefaultMviModel<ModalDrawerMviModel.Intent, ModalDrawerMviModel.UiState, ModalDrawerMviModel.Effect>(
|
||||
initialState = ModalDrawerMviModel.UiState(),
|
||||
) {
|
||||
private var currentPage = 1
|
||||
private val searchEventChannel = Channel<Unit>()
|
||||
|
||||
init {
|
||||
@ -116,16 +117,17 @@ class ModalDrawerViewModel(
|
||||
|
||||
override fun reduce(intent: ModalDrawerMviModel.Intent) {
|
||||
when (intent) {
|
||||
ModalDrawerMviModel.Intent.Refresh ->
|
||||
screenModelScope.launch {
|
||||
refresh()
|
||||
}
|
||||
ModalDrawerMviModel.Intent.Refresh -> screenModelScope.launch {
|
||||
refresh()
|
||||
}
|
||||
|
||||
is ModalDrawerMviModel.Intent.SetSearch -> {
|
||||
screenModelScope.launch {
|
||||
updateState { it.copy(searchText = intent.value) }
|
||||
searchEventChannel.send(Unit)
|
||||
}
|
||||
is ModalDrawerMviModel.Intent.SetSearch -> screenModelScope.launch {
|
||||
updateState { it.copy(searchText = intent.value) }
|
||||
searchEventChannel.send(Unit)
|
||||
}
|
||||
|
||||
ModalDrawerMviModel.Intent.LoadNextPage -> screenModelScope.launch {
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,33 +156,18 @@ class ModalDrawerViewModel(
|
||||
if (uiState.value.refreshing) {
|
||||
return
|
||||
}
|
||||
updateState { it.copy(refreshing = true) }
|
||||
currentPage = 1
|
||||
updateState {
|
||||
it.copy(
|
||||
refreshing = true,
|
||||
canFetchMore = true,
|
||||
loading = false,
|
||||
)
|
||||
}
|
||||
|
||||
val auth = identityRepository.authToken.value
|
||||
val accountId = accountRepository.getActive()?.id
|
||||
val favoriteCommunityIds =
|
||||
favoriteCommunityRepository.getAll(accountId).map { it.communityId }
|
||||
val searchText = uiState.value.searchText
|
||||
val communities =
|
||||
communityRepository.getSubscribed(auth)
|
||||
.let {
|
||||
if (searchText.isEmpty()) {
|
||||
it
|
||||
} else {
|
||||
it.filter { e ->
|
||||
listOf(e.name, e.title).any { s -> s.contains(other = searchText, ignoreCase = true) }
|
||||
}
|
||||
}
|
||||
}.map { community ->
|
||||
community.copy(favorite = community.id in favoriteCommunityIds)
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
.let {
|
||||
val favorites = it.filter { e -> e.favorite }
|
||||
val res = it - favorites.toSet()
|
||||
favorites + res
|
||||
}
|
||||
val multiCommunitites =
|
||||
val multiCommunities =
|
||||
accountId?.let {
|
||||
multiCommunityRepository.getAll(it)
|
||||
.let { communities ->
|
||||
@ -192,12 +179,52 @@ class ModalDrawerViewModel(
|
||||
}
|
||||
.sortedBy { e -> e.name }
|
||||
}.orEmpty()
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
multiCommunities = multiCommunities,
|
||||
)
|
||||
}
|
||||
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
private suspend fun loadNextPage() {
|
||||
val currentState = uiState.value
|
||||
if (!currentState.canFetchMore || currentState.loading) {
|
||||
updateState { it.copy(refreshing = false) }
|
||||
return
|
||||
}
|
||||
val accountId = accountRepository.getActive()?.id
|
||||
val auth = identityRepository.authToken.value
|
||||
val searchText = uiState.value.searchText
|
||||
val favoriteCommunityIds =
|
||||
favoriteCommunityRepository.getAll(accountId).map { it.communityId }
|
||||
val itemsToAdd = communityRepository.getSubscribed(
|
||||
auth = auth,
|
||||
page = currentPage,
|
||||
query = searchText,
|
||||
).map { community ->
|
||||
community.copy(favorite = community.id in favoriteCommunityIds)
|
||||
}.sortedBy { it.name }.let {
|
||||
val favorites = it.filter { e -> e.favorite }
|
||||
val res = it - favorites.toSet()
|
||||
favorites + res
|
||||
}
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
currentPage++
|
||||
}
|
||||
updateState {
|
||||
it.copy(
|
||||
isFiltering = searchText.isNotEmpty(),
|
||||
refreshing = false,
|
||||
communities = communities,
|
||||
multiCommunities = multiCommunitites,
|
||||
communities = if (currentState.refreshing) {
|
||||
itemsToAdd
|
||||
} else {
|
||||
currentState.communities + itemsToAdd
|
||||
},
|
||||
canFetchMore = itemsToAdd.isNotEmpty(),
|
||||
loading = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,8 @@ interface ManageSubscriptionsMviModel :
|
||||
data class ToggleFavorite(val id: Long) : Intent
|
||||
|
||||
data class SetSearch(val value: String) : Intent
|
||||
|
||||
data object LoadNextPage : Intent
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
@ -32,6 +34,8 @@ interface ManageSubscriptionsMviModel :
|
||||
val autoLoadImages: Boolean = true,
|
||||
val preferNicknames: Boolean = true,
|
||||
val searchText: String = "",
|
||||
val canFetchMore: Boolean = true,
|
||||
val loading: Boolean = false,
|
||||
)
|
||||
|
||||
sealed interface Effect {
|
||||
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
@ -29,6 +30,7 @@ import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -60,6 +62,7 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.koin.getScreenModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||
@ -431,6 +434,23 @@ class ManageSubscriptionsScreen : Screen {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||
model.reduce(ManageSubscriptionsMviModel.Intent.LoadNextPage)
|
||||
}
|
||||
if (uiState.loading && !uiState.refreshing) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(25.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!uiState.initial) {
|
||||
|
@ -36,6 +36,7 @@ class ManageSubscriptionsViewModel(
|
||||
DefaultMviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect>(
|
||||
initialState = ManageSubscriptionsMviModel.UiState(),
|
||||
) {
|
||||
private var currentPage = 1
|
||||
private val searchEventChannel = Channel<Unit>()
|
||||
|
||||
init {
|
||||
@ -61,16 +62,16 @@ class ManageSubscriptionsViewModel(
|
||||
emitEffect(ManageSubscriptionsMviModel.Effect.BackToTop)
|
||||
refresh()
|
||||
}.launchIn(this)
|
||||
}
|
||||
if (uiState.value.communities.isEmpty()) {
|
||||
refresh()
|
||||
if (uiState.value.communities.isEmpty()) {
|
||||
refresh(initial = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reduce(intent: ManageSubscriptionsMviModel.Intent) {
|
||||
when (intent) {
|
||||
ManageSubscriptionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
|
||||
ManageSubscriptionsMviModel.Intent.Refresh -> refresh()
|
||||
ManageSubscriptionsMviModel.Intent.Refresh -> screenModelScope.launch { refresh() }
|
||||
is ManageSubscriptionsMviModel.Intent.Unsubscribe -> {
|
||||
uiState.value.communities.firstOrNull { it.id == intent.id }?.also { community ->
|
||||
unsubscribe(community)
|
||||
@ -92,57 +93,43 @@ class ManageSubscriptionsViewModel(
|
||||
}
|
||||
|
||||
is ManageSubscriptionsMviModel.Intent.SetSearch -> updateSearchText(intent.value)
|
||||
|
||||
ManageSubscriptionsMviModel.Intent.LoadNextPage -> screenModelScope.launch {
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
if (uiState.value.refreshing) {
|
||||
return
|
||||
private suspend fun refresh(initial: Boolean = false) {
|
||||
updateState {
|
||||
it.copy(
|
||||
refreshing = true,
|
||||
initial = initial,
|
||||
canFetchMore = true,
|
||||
)
|
||||
}
|
||||
screenModelScope.launch {
|
||||
updateState { it.copy(refreshing = true) }
|
||||
val auth = identityRepository.authToken.value
|
||||
val accountId = accountRepository.getActive()?.id ?: 0L
|
||||
val favoriteCommunityIds =
|
||||
favoriteCommunityRepository.getAll(accountId).map { it.communityId }
|
||||
val communities =
|
||||
communityRepository.getSubscribed(auth)
|
||||
.let {
|
||||
val searchText = uiState.value.searchText
|
||||
if (searchText.isNotEmpty()) {
|
||||
it.filter { c ->
|
||||
c.title.contains(searchText, ignoreCase = true) ||
|
||||
c.name.contains(searchText, ignoreCase = true)
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.map { community ->
|
||||
community.copy(favorite = community.id in favoriteCommunityIds)
|
||||
}.sortedBy { it.name }
|
||||
val multiCommunitites =
|
||||
multiCommunityRepository.getAll(accountId)
|
||||
.let {
|
||||
val searchText = uiState.value.searchText
|
||||
if (searchText.isNotEmpty()) {
|
||||
it.filter { c ->
|
||||
c.name.contains(searchText, ignoreCase = true)
|
||||
}
|
||||
} else {
|
||||
it
|
||||
currentPage = 1
|
||||
val accountId = accountRepository.getActive()?.id ?: 0L
|
||||
val multiCommunitites =
|
||||
multiCommunityRepository.getAll(accountId)
|
||||
.let {
|
||||
val searchText = uiState.value.searchText
|
||||
if (searchText.isNotEmpty()) {
|
||||
it.filter { c ->
|
||||
c.name.contains(searchText, ignoreCase = true)
|
||||
}
|
||||
} else {
|
||||
it
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
}
|
||||
.sortedBy { it.name }
|
||||
|
||||
updateState {
|
||||
it.copy(
|
||||
refreshing = false,
|
||||
initial = false,
|
||||
communities = communities,
|
||||
multiCommunities = multiCommunitites,
|
||||
)
|
||||
}
|
||||
updateState {
|
||||
it.copy(
|
||||
multiCommunities = multiCommunitites,
|
||||
)
|
||||
}
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
private fun unsubscribe(community: CommunityModel) {
|
||||
@ -229,4 +216,44 @@ class ManageSubscriptionsViewModel(
|
||||
searchEventChannel.send(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadNextPage() {
|
||||
val currentState = uiState.value
|
||||
if (!currentState.canFetchMore || currentState.loading) {
|
||||
updateState { it.copy(refreshing = false) }
|
||||
return
|
||||
}
|
||||
val accountId = accountRepository.getActive()?.id
|
||||
val auth = identityRepository.authToken.value
|
||||
val searchText = uiState.value.searchText
|
||||
val favoriteCommunityIds =
|
||||
favoriteCommunityRepository.getAll(accountId).map { it.communityId }
|
||||
val itemsToAdd = communityRepository.getSubscribed(
|
||||
auth = auth,
|
||||
page = currentPage,
|
||||
query = searchText,
|
||||
).map { community ->
|
||||
community.copy(favorite = community.id in favoriteCommunityIds)
|
||||
}.sortedBy { it.name }.let {
|
||||
val favorites = it.filter { e -> e.favorite }
|
||||
val res = it - favorites.toSet()
|
||||
favorites + res
|
||||
}
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
currentPage++
|
||||
}
|
||||
updateState {
|
||||
it.copy(
|
||||
refreshing = false,
|
||||
communities = if (currentState.refreshing) {
|
||||
itemsToAdd
|
||||
} else {
|
||||
currentState.communities + itemsToAdd
|
||||
},
|
||||
canFetchMore = itemsToAdd.isNotEmpty(),
|
||||
loading = false,
|
||||
initial = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ interface MultiCommunityEditorMviModel :
|
||||
|
||||
data class ToggleCommunity(val id: Long) : Intent
|
||||
|
||||
data object LoadNextPage : Intent
|
||||
|
||||
data object Submit : Intent
|
||||
}
|
||||
|
||||
@ -27,8 +29,12 @@ interface MultiCommunityEditorMviModel :
|
||||
val nameError: ValidationError? = null,
|
||||
val icon: String? = null,
|
||||
val availableIcons: List<String> = emptyList(),
|
||||
val communities: List<Pair<CommunityModel, Boolean>> = emptyList(),
|
||||
val communities: List<CommunityModel> = emptyList(),
|
||||
val selectedCommunityIds: List<Long> = emptyList(),
|
||||
val searchText: String = "",
|
||||
val refreshing: Boolean = false,
|
||||
val loading: Boolean = false,
|
||||
val canFetchMore: Boolean = true,
|
||||
)
|
||||
|
||||
sealed interface Effect {
|
||||
|
@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
@ -348,9 +349,8 @@ class MultiCommunityEditorScreen(
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
|
||||
) {
|
||||
items(uiState.communities) { communityItem ->
|
||||
val community = communityItem.first
|
||||
val selected = communityItem.second
|
||||
items(uiState.communities) { community ->
|
||||
val selected = uiState.selectedCommunityIds.contains(community.id)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
@ -375,6 +375,23 @@ class MultiCommunityEditorScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
if (!uiState.loading && uiState.canFetchMore) {
|
||||
model.reduce(MultiCommunityEditorMviModel.Intent.LoadNextPage)
|
||||
}
|
||||
if (uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(25.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.Mult
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.ValidationError
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@ -33,7 +32,7 @@ class MultiCommunityEditorViewModel(
|
||||
DefaultMviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect>(
|
||||
initialState = MultiCommunityEditorMviModel.UiState(),
|
||||
) {
|
||||
private var communities: List<Pair<CommunityModel, Boolean>> = emptyList()
|
||||
private var currentPage = 1
|
||||
private val searchEventChannel = Channel<Unit>()
|
||||
|
||||
init {
|
||||
@ -48,14 +47,19 @@ class MultiCommunityEditorViewModel(
|
||||
}.launchIn(this)
|
||||
|
||||
searchEventChannel.receiveAsFlow().debounce(1000).onEach {
|
||||
updateState {
|
||||
val filtered = filterCommunities()
|
||||
it.copy(communities = filtered)
|
||||
}
|
||||
refresh()
|
||||
}.launchIn(this)
|
||||
}
|
||||
if (communities.isEmpty()) {
|
||||
populate()
|
||||
|
||||
if (uiState.value.communities.isEmpty()) {
|
||||
val editedCommunity =
|
||||
communityId?.let {
|
||||
multiCommunityRepository.getById(it)
|
||||
}
|
||||
updateState {
|
||||
it.copy(selectedCommunityIds = editedCommunity?.communityIds.orEmpty())
|
||||
}
|
||||
refresh(initial = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,34 +72,62 @@ class MultiCommunityEditorViewModel(
|
||||
}
|
||||
is MultiCommunityEditorMviModel.Intent.ToggleCommunity -> toggleCommunity(intent.id)
|
||||
is MultiCommunityEditorMviModel.Intent.SetSearch -> setSearch(intent.value)
|
||||
MultiCommunityEditorMviModel.Intent.LoadNextPage -> screenModelScope.launch {
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
MultiCommunityEditorMviModel.Intent.Submit -> submit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
screenModelScope.launch {
|
||||
val editedCommunity =
|
||||
communityId?.let {
|
||||
multiCommunityRepository.getById(it)
|
||||
}
|
||||
val auth = identityRepository.authToken.value
|
||||
communities =
|
||||
communityRepository.getSubscribed(auth).sortedBy { it.name }.map { c ->
|
||||
c to (editedCommunity?.communityIds?.contains(c.id) == true)
|
||||
}
|
||||
updateState {
|
||||
val newCommunities = communities
|
||||
val availableIcons = newCommunities.filter { i -> i.second }.mapNotNull { i -> i.first.icon }
|
||||
it.copy(
|
||||
communities = newCommunities,
|
||||
name = editedCommunity?.name.orEmpty(),
|
||||
icon = editedCommunity?.icon,
|
||||
availableIcons = availableIcons,
|
||||
)
|
||||
}
|
||||
private suspend fun refresh(initial: Boolean = false) {
|
||||
val editedCommunity = communityId?.let {
|
||||
multiCommunityRepository.getById(it)
|
||||
}
|
||||
currentPage = 1
|
||||
updateState {
|
||||
it.copy(
|
||||
name = editedCommunity?.name.orEmpty(),
|
||||
icon = editedCommunity?.icon,
|
||||
refreshing = !initial,
|
||||
loading = false,
|
||||
canFetchMore = true,
|
||||
)
|
||||
}
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
private suspend fun loadNextPage() {
|
||||
val currentState = uiState.value
|
||||
if (!currentState.canFetchMore || currentState.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
val auth = identityRepository.authToken.value
|
||||
val searchText = uiState.value.searchText
|
||||
val itemsToAdd = communityRepository.getSubscribed(
|
||||
auth = auth,
|
||||
page = currentPage,
|
||||
query = searchText,
|
||||
)
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
currentPage++
|
||||
}
|
||||
updateState {
|
||||
it.copy(
|
||||
communities = if (currentState.refreshing) {
|
||||
itemsToAdd
|
||||
} else {
|
||||
currentState.communities + itemsToAdd
|
||||
},
|
||||
canFetchMore = itemsToAdd.isNotEmpty(),
|
||||
loading = false,
|
||||
refreshing = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun setSearch(value: String) {
|
||||
screenModelScope.launch {
|
||||
updateState { it.copy(searchText = value) }
|
||||
@ -103,17 +135,6 @@ class MultiCommunityEditorViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterCommunities(): List<Pair<CommunityModel, Boolean>> {
|
||||
val searchText = uiState.value.searchText
|
||||
val res =
|
||||
if (searchText.isNotEmpty()) {
|
||||
communities.filter { it.first.name.contains(other = searchText, ignoreCase = true) }
|
||||
} else {
|
||||
communities
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
private fun selectImage(index: Int?) {
|
||||
screenModelScope.launch {
|
||||
val image =
|
||||
@ -127,28 +148,26 @@ class MultiCommunityEditorViewModel(
|
||||
}
|
||||
|
||||
private fun toggleCommunity(communityId: Long) {
|
||||
val newCommunities =
|
||||
communities.map { item ->
|
||||
if (item.first.id == communityId) {
|
||||
item.first to !item.second
|
||||
} else {
|
||||
item
|
||||
}
|
||||
}
|
||||
val availableIcons =
|
||||
newCommunities.filter { i ->
|
||||
i.second
|
||||
}.mapNotNull { i ->
|
||||
i.first.icon
|
||||
}
|
||||
communities = newCommunities
|
||||
val filtered = filterCommunities()
|
||||
screenModelScope.launch {
|
||||
updateState { state ->
|
||||
state.copy(
|
||||
communities = filtered,
|
||||
availableIcons = availableIcons,
|
||||
)
|
||||
val currentCommunityIds = uiState.value.selectedCommunityIds
|
||||
val shouldBeRemoved = currentCommunityIds.contains(communityId)
|
||||
val iconUrl = uiState.value.communities.firstOrNull { c -> c.id == communityId }?.icon
|
||||
if (shouldBeRemoved) {
|
||||
updateState {
|
||||
it.copy(
|
||||
selectedCommunityIds = currentCommunityIds.filter { id -> id != communityId },
|
||||
availableIcons = it.availableIcons.filter { url -> url != iconUrl }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
updateState {
|
||||
it.copy(
|
||||
selectedCommunityIds = currentCommunityIds + communityId,
|
||||
availableIcons = if (iconUrl != null) {
|
||||
it.availableIcons + iconUrl
|
||||
} else it.availableIcons
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,7 +191,7 @@ class MultiCommunityEditorViewModel(
|
||||
|
||||
screenModelScope.launch {
|
||||
val icon = currentState.icon
|
||||
val communityIds = currentState.communities.filter { it.second }.map { it.first.id }
|
||||
val communityIds = currentState.selectedCommunityIds
|
||||
val editedCommunity =
|
||||
communityId?.let {
|
||||
multiCommunityRepository.getById(it)
|
||||
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@ -18,6 +19,7 @@ import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@ -150,6 +152,23 @@ class SelectCommunityDialog : Screen {
|
||||
community = community,
|
||||
)
|
||||
}
|
||||
|
||||
item {
|
||||
if (!uiState.initial && !uiState.loading && uiState.canFetchMore) {
|
||||
model.reduce(SelectCommunityMviModel.Intent.LoadNextPage)
|
||||
}
|
||||
if (!uiState.initial && uiState.loading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(25.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ interface SelectCommunityMviModel :
|
||||
ScreenModel {
|
||||
sealed interface Intent {
|
||||
data class SetSearch(val value: String) : Intent
|
||||
|
||||
data object LoadNextPage : Intent
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
@ -17,6 +19,9 @@ interface SelectCommunityMviModel :
|
||||
val searchText: String = "",
|
||||
val autoLoadImages: Boolean = true,
|
||||
val preferNicknames: Boolean = true,
|
||||
val loading: Boolean = false,
|
||||
val refreshing: Boolean = false,
|
||||
val canFetchMore: Boolean = true,
|
||||
)
|
||||
|
||||
sealed interface Effect
|
||||
|
@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
class SelectCommunityViewModel(
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val communityRepository: CommunityRepository,
|
||||
@ -25,7 +24,7 @@ class SelectCommunityViewModel(
|
||||
DefaultMviModel<SelectCommunityMviModel.Intent, SelectCommunityMviModel.UiState, SelectCommunityMviModel.Effect>(
|
||||
initialState = SelectCommunityMviModel.UiState(),
|
||||
) {
|
||||
private var communities: List<CommunityModel> = emptyList()
|
||||
private var currentPage = 1
|
||||
private val searchEventChannel = Channel<Unit>()
|
||||
|
||||
init {
|
||||
@ -39,21 +38,23 @@ class SelectCommunityViewModel(
|
||||
}
|
||||
}.launchIn(this)
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
searchEventChannel.receiveAsFlow().debounce(1000).onEach {
|
||||
updateState {
|
||||
val filtered = filterCommunities()
|
||||
it.copy(communities = filtered)
|
||||
}
|
||||
refresh()
|
||||
}.launchIn(this)
|
||||
}
|
||||
if (communities.isEmpty()) {
|
||||
populate()
|
||||
|
||||
if (uiState.value.initial) {
|
||||
refresh(initial = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reduce(intent: SelectCommunityMviModel.Intent) {
|
||||
when (intent) {
|
||||
is SelectCommunityMviModel.Intent.SetSearch -> setSearch(intent.value)
|
||||
SelectCommunityMviModel.Intent.LoadNextPage -> screenModelScope.launch {
|
||||
loadNextPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,27 +65,46 @@ class SelectCommunityViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
screenModelScope.launch(Dispatchers.IO) {
|
||||
val auth = identityRepository.authToken.value
|
||||
communities = communityRepository.getSubscribed(auth).sortedBy { it.name }
|
||||
updateState {
|
||||
it.copy(
|
||||
initial = false,
|
||||
communities = communities,
|
||||
)
|
||||
}
|
||||
private suspend fun refresh(initial: Boolean = false) {
|
||||
currentPage = 1
|
||||
updateState {
|
||||
it.copy(
|
||||
initial = initial,
|
||||
canFetchMore = true,
|
||||
loading = false,
|
||||
refreshing = true,
|
||||
)
|
||||
}
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
private fun filterCommunities(): List<CommunityModel> {
|
||||
private suspend fun loadNextPage() {
|
||||
val currentState = uiState.value
|
||||
if (!currentState.canFetchMore || currentState.loading) {
|
||||
return
|
||||
}
|
||||
val auth = identityRepository.authToken.value
|
||||
val searchText = uiState.value.searchText
|
||||
val res =
|
||||
if (searchText.isNotEmpty()) {
|
||||
communities.filter { it.name.contains(searchText) }
|
||||
} else {
|
||||
communities
|
||||
}
|
||||
return res
|
||||
val itemsToAdd = communityRepository.getSubscribed(
|
||||
auth = auth,
|
||||
page = currentPage,
|
||||
query = searchText,
|
||||
)
|
||||
if (itemsToAdd.isNotEmpty()) {
|
||||
currentPage++
|
||||
}
|
||||
updateState {
|
||||
it.copy(
|
||||
communities = if (currentState.refreshing) {
|
||||
itemsToAdd
|
||||
} else {
|
||||
currentState.communities + itemsToAdd
|
||||
},
|
||||
canFetchMore = itemsToAdd.isNotEmpty(),
|
||||
loading = false,
|
||||
initial = false,
|
||||
refreshing = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user