feat(explore): subscription management screen

This commit is contained in:
Diego Beraldin 2023-09-27 12:43:37 +02:00
parent d2500372c2
commit 6297ef3b97
16 changed files with 287 additions and 17 deletions

View File

@ -42,6 +42,7 @@ kotlin {
implementation(projects.coreUtils) implementation(projects.coreUtils)
implementation(projects.corePreferences) implementation(projects.corePreferences)
implementation(projects.coreCommonui) implementation(projects.coreCommonui)
implementation(projects.coreCommonui.components)
implementation(projects.coreNotifications) implementation(projects.coreNotifications)
implementation(projects.domainIdentity) implementation(projects.domainIdentity)

View File

@ -1,9 +1,16 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import org.koin.java.KoinJavaComponent.inject import org.koin.java.KoinJavaComponent.inject
actual fun getExploreViewModel(): ExploreViewModel { actual fun getExploreViewModel(): ExploreViewModel {
val res: ExploreViewModel by inject(ExploreViewModel::class.java) val res: ExploreViewModel by inject(ExploreViewModel::class.java)
return res return res
} }
actual fun getManageSubscriptionsViewModel(): ManageSubscriptionsViewModel {
val res: ManageSubscriptionsViewModel by inject(ManageSubscriptionsViewModel::class.java)
return res
}

View File

@ -1,8 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreMviModel import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import org.koin.dsl.module import org.koin.dsl.module
val searchTabModule = module { val searchTabModule = module {
@ -20,4 +22,12 @@ val searchTabModule = module {
hapticFeedback = get(), hapticFeedback = get(),
) )
} }
factory {
ManageSubscriptionsViewModel(
mvi = DefaultMviModel(ManageSubscriptionsMviModel.UiState()),
identityRepository = get(),
communityRepository = get(),
hapticFeedback = get(),
)
}
} }

View File

@ -1,5 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
expect fun getExploreViewModel(): ExploreViewModel expect fun getExploreViewModel(): ExploreViewModel
expect fun getManageSubscriptionsViewModel(): ManageSubscriptionsViewModel

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.content package com.github.diegoberaldin.raccoonforlemmy.feature.search.main
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel

View File

@ -1,4 +1,4 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.content package com.github.diegoberaldin.raccoonforlemmy.feature.search.main
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -73,6 +73,7 @@ import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SearchResultTy
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.UserModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getExploreViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsScreen
import com.github.diegoberaldin.raccoonforlemmy.resources.MR import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
@ -101,6 +102,7 @@ class ExploreScreen : Screen {
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
listingType = uiState.listingType, listingType = uiState.listingType,
sortType = uiState.sortType, sortType = uiState.sortType,
isLogged = uiState.isLogged,
onSelectListingType = { onSelectListingType = {
val sheet = ListingTypeBottomSheet( val sheet = ListingTypeBottomSheet(
isLogged = uiState.isLogged, isLogged = uiState.isLogged,
@ -125,6 +127,10 @@ class ExploreScreen : Screen {
}, key, NotificationCenterContractKeys.ChangeSortType) }, key, NotificationCenterContractKeys.ChangeSortType)
bottomSheetNavigator.show(sheet) bottomSheetNavigator.show(sheet)
}, },
onSettings = {
val sheet = ManageSubscriptionsScreen()
navigator?.push(sheet)
},
) )
}, },
) { padding -> ) { padding ->

View File

@ -1,10 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.content package com.github.diegoberaldin.raccoonforlemmy.feature.search.main
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ManageAccounts
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -29,8 +31,10 @@ internal fun CommunityTopBar(
scrollBehavior: TopAppBarScrollBehavior? = null, scrollBehavior: TopAppBarScrollBehavior? = null,
listingType: ListingType, listingType: ListingType,
sortType: SortType, sortType: SortType,
onSelectListingType: () -> Unit, isLogged: Boolean = false,
onSelectSortType: () -> Unit, onSelectListingType: (() -> Unit)? = null,
onSelectSortType: (() -> Unit)? = null,
onSettings: (() -> Unit)? = null,
) { ) {
TopAppBar( TopAppBar(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
@ -40,7 +44,7 @@ internal fun CommunityTopBar(
navigationIcon = { navigationIcon = {
Image( Image(
modifier = Modifier.onClick { modifier = Modifier.onClick {
onSelectListingType() onSelectListingType?.invoke()
}, },
imageVector = listingType.toIcon(), imageVector = listingType.toIcon(),
contentDescription = null, contentDescription = null,
@ -63,7 +67,9 @@ internal fun CommunityTopBar(
} }
}, },
actions = { actions = {
Row { Row(
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
val additionalLabel = when (sortType) { val additionalLabel = when (sortType) {
SortType.Top.Day -> stringResource(MR.strings.home_sort_type_top_day_short) SortType.Top.Day -> stringResource(MR.strings.home_sort_type_top_day_short)
SortType.Top.Month -> stringResource(MR.strings.home_sort_type_top_month_short) SortType.Top.Month -> stringResource(MR.strings.home_sort_type_top_month_short)
@ -85,12 +91,23 @@ internal fun CommunityTopBar(
} }
Image( Image(
modifier = Modifier.onClick { modifier = Modifier.onClick {
onSelectSortType() onSelectSortType?.invoke()
}, },
imageVector = sortType.toIcon(), imageVector = sortType.toIcon(),
contentDescription = null, contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground), colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
) )
if (isLogged) {
Image(
modifier = Modifier.onClick {
onSettings?.invoke()
},
imageVector = Icons.Default.ManageAccounts,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
}
} }
}, },
) )

View File

@ -1,7 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.content package com.github.diegoberaldin.raccoonforlemmy.feature.search.main
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
@ -9,6 +8,7 @@ import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationC
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys import com.github.diegoberaldin.raccoonforlemmy.core.preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore import com.github.diegoberaldin.raccoonforlemmy.core.preferences.TemporaryKeyStore
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.ApiConfigurationRepository
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.CommentModel import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommentModel

View File

@ -0,0 +1,20 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
interface ManageSubscriptionsMviModel :
MviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect> {
sealed interface Intent {
data object HapticIndication : Intent
data class Unsubscribe(val index: Int) : Intent
}
data class UiState(
val loading: Boolean = false,
val communities: List<CommunityModel> = emptyList(),
)
sealed interface Effect
}

View File

@ -0,0 +1,136 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissValue
import androidx.compose.material.Icon
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Unsubscribe
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommunityItem
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.ProgressHud
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SwipeableCard
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getManageSubscriptionsViewModel
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import dev.icerock.moko.resources.compose.stringResource
class ManageSubscriptionsScreen : Screen {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val model = rememberScreenModel { getManageSubscriptionsViewModel() }
model.bindToLifecycle(key)
val uiState by model.uiState.collectAsState()
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
text = stringResource(MR.strings.explore_title_manage_subscriptions),
style = MaterialTheme.typography.titleLarge
)
},
scrollBehavior = scrollBehavior,
navigationIcon = {
Image(
modifier = Modifier.onClick {
navigator?.pop()
},
imageVector = Icons.Default.ArrowBack,
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
)
},
)
},
) { paddingValues ->
Box(
modifier = Modifier.padding(paddingValues),
) {
LazyColumn(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
itemsIndexed(uiState.communities) { idx, community ->
SwipeableCard(
modifier = Modifier.fillMaxWidth(),
directions = setOf(DismissDirection.EndToStart),
backgroundColor = {
when (it) {
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
else -> Color.Transparent
}
},
onGestureBegin = {
model.reduce(ManageSubscriptionsMviModel.Intent.HapticIndication)
},
onDismissToStart = {
model.reduce(
ManageSubscriptionsMviModel.Intent.Unsubscribe(idx),
)
},
swipeContent = { _ ->
Icon(
modifier = Modifier.background(
color = MaterialTheme.colorScheme.onSecondary,
shape = CircleShape,
).padding(Spacing.xs),
imageVector = Icons.Default.Unsubscribe,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
)
},
content = {
CommunityItem(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.onClick {
navigator?.push(
CommunityDetailScreen(community),
)
},
community = community,
)
},
)
}
}
if (uiState.loading) {
ProgressHud()
}
}
}
}
}

View File

@ -0,0 +1,63 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions
import cafe.adriel.voyager.core.model.ScreenModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
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.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class ManageSubscriptionsViewModel(
private val mvi: DefaultMviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect>,
private val identityRepository: IdentityRepository,
private val communityRepository: CommunityRepository,
private val hapticFeedback: HapticFeedback,
) : ScreenModel,
MviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect> by mvi {
override fun onStarted() {
mvi.onStarted()
if (uiState.value.communities.isEmpty()) {
refresh()
}
}
override fun reduce(intent: ManageSubscriptionsMviModel.Intent) {
when (intent) {
ManageSubscriptionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
is ManageSubscriptionsMviModel.Intent.Unsubscribe -> handleUnsubscription(
community = uiState.value.communities[intent.index]
)
}
}
private fun refresh() {
mvi.updateState { it.copy(loading = true) }
mvi.scope?.launch(Dispatchers.IO) {
val auth = identityRepository.authToken.value
val items = communityRepository.getSubscribed(auth).sortedBy { it.name }
mvi.updateState {
it.copy(
loading = false,
communities = items,
)
}
}
}
private fun handleUnsubscription(community: CommunityModel) {
mvi.scope?.launch {
val auth = identityRepository.authToken.value
communityRepository.unsubscribe(
auth = auth, id = community.id
)
mvi.updateState {
it.copy(communities = it.communities.filter { c -> c.id != community.id })
}
}
}
}

View File

@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.Tab
import cafe.adriel.voyager.navigator.tab.TabOptions import cafe.adriel.voyager.navigator.tab.TabOptions
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreScreen import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreScreen
import com.github.diegoberaldin.raccoonforlemmy.resources.MR import com.github.diegoberaldin.raccoonforlemmy.resources.MR
import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository import com.github.diegoberaldin.raccoonforlemmy.resources.di.getLanguageRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.di.staticString import com.github.diegoberaldin.raccoonforlemmy.resources.di.staticString

View File

@ -1,11 +1,15 @@
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreViewModel import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
actual fun getExploreViewModel() = SearchScreenModelHelper.model actual fun getExploreViewModel() = SearchScreenModelHelper.model
actual fun getManageSubscriptionsViewModel() = SearchScreenModelHelper.manageSuscriptionsViewModel
object SearchScreenModelHelper : KoinComponent { object SearchScreenModelHelper : KoinComponent {
val model: ExploreViewModel by inject() val model: ExploreViewModel by inject()
val manageSuscriptionsViewModel: ManageSubscriptionsViewModel by inject()
} }

View File

@ -63,6 +63,7 @@
<string name="explore_result_type_communities">Communities</string> <string name="explore_result_type_communities">Communities</string>
<string name="explore_result_type_users">Users</string> <string name="explore_result_type_users">Users</string>
<string name="explore_search_placeholder">Search text</string> <string name="explore_search_placeholder">Search text</string>
<string name="explore_title_manage_subscriptions">Manage subscriptions</string>
<string name="profile_not_logged_message">You are currently not logged in.\nPlease add an <string name="profile_not_logged_message">You are currently not logged in.\nPlease add an
account to continue. account to continue.

View File

@ -59,6 +59,7 @@
<string name="explore_result_type_communities">Comunidades</string> <string name="explore_result_type_communities">Comunidades</string>
<string name="explore_result_type_users">Usuarios</string> <string name="explore_result_type_users">Usuarios</string>
<string name="explore_search_placeholder">Texto de búsqueda</string> <string name="explore_search_placeholder">Texto de búsqueda</string>
<string name="explore_title_manage_subscriptions">Gestionar subscripciones</string>
<string name="profile_not_logged_message">Acceso no efectuado.\nAñadir una cuenta para <string name="profile_not_logged_message">Acceso no efectuado.\nAñadir una cuenta para
continuar. continuar.

View File

@ -59,6 +59,7 @@
<string name="explore_result_type_communities">Comunità</string> <string name="explore_result_type_communities">Comunità</string>
<string name="explore_result_type_users">Utenti</string> <string name="explore_result_type_users">Utenti</string>
<string name="explore_search_placeholder">Testo di ricerca</string> <string name="explore_search_placeholder">Testo di ricerca</string>
<string name="explore_title_manage_subscriptions">Gestione iscruzione</string>
<string name="profile_not_logged_message">Login non effettuato.\nAggiungi un account per <string name="profile_not_logged_message">Login non effettuato.\nAggiungi un account per
continuare. continuare.