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.corePreferences)
implementation(projects.coreCommonui)
implementation(projects.coreCommonui.components)
implementation(projects.coreNotifications)
implementation(projects.domainIdentity)

View File

@ -1,9 +1,16 @@
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
actual fun getExploreViewModel(): ExploreViewModel {
val res: ExploreViewModel by inject(ExploreViewModel::class.java)
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
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreMviModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.content.ExploreViewModel
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreMviModel
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
val searchTabModule = module {
@ -20,4 +22,12 @@ val searchTabModule = module {
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
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.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.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.UserModel
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 dev.icerock.moko.resources.compose.stringResource
@ -101,6 +102,7 @@ class ExploreScreen : Screen {
scrollBehavior = scrollBehavior,
listingType = uiState.listingType,
sortType = uiState.sortType,
isLogged = uiState.isLogged,
onSelectListingType = {
val sheet = ListingTypeBottomSheet(
isLogged = uiState.isLogged,
@ -125,6 +127,10 @@ class ExploreScreen : Screen {
}, key, NotificationCenterContractKeys.ChangeSortType)
bottomSheetNavigator.show(sheet)
},
onSettings = {
val sheet = ManageSubscriptionsScreen()
navigator?.push(sheet)
},
)
},
) { 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.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.MaterialTheme
import androidx.compose.material3.Text
@ -29,8 +31,10 @@ internal fun CommunityTopBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
listingType: ListingType,
sortType: SortType,
onSelectListingType: () -> Unit,
onSelectSortType: () -> Unit,
isLogged: Boolean = false,
onSelectListingType: (() -> Unit)? = null,
onSelectSortType: (() -> Unit)? = null,
onSettings: (() -> Unit)? = null,
) {
TopAppBar(
scrollBehavior = scrollBehavior,
@ -40,7 +44,7 @@ internal fun CommunityTopBar(
navigationIcon = {
Image(
modifier = Modifier.onClick {
onSelectListingType()
onSelectListingType?.invoke()
},
imageVector = listingType.toIcon(),
contentDescription = null,
@ -63,7 +67,9 @@ internal fun CommunityTopBar(
}
},
actions = {
Row {
Row(
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
) {
val additionalLabel = when (sortType) {
SortType.Top.Day -> stringResource(MR.strings.home_sort_type_top_day_short)
SortType.Top.Month -> stringResource(MR.strings.home_sort_type_top_month_short)
@ -85,12 +91,23 @@ internal fun CommunityTopBar(
}
Image(
modifier = Modifier.onClick {
onSelectSortType()
onSelectSortType?.invoke()
},
imageVector = sortType.toIcon(),
contentDescription = null,
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 com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
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.preferences.KeyStoreKeys
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.IdentityRepository
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.tab.Tab
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.di.getLanguageRepository
import com.github.diegoberaldin.raccoonforlemmy.resources.di.staticString

View File

@ -1,11 +1,15 @@
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.inject
actual fun getExploreViewModel() = SearchScreenModelHelper.model
actual fun getManageSubscriptionsViewModel() = SearchScreenModelHelper.manageSuscriptionsViewModel
object SearchScreenModelHelper : KoinComponent {
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_users">Users</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
account to continue.

View File

@ -59,6 +59,7 @@
<string name="explore_result_type_communities">Comunidades</string>
<string name="explore_result_type_users">Usuarios</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
continuar.

View File

@ -59,6 +59,7 @@
<string name="explore_result_type_communities">Comunità</string>
<string name="explore_result_type_users">Utenti</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
continuare.