From 7fd71057551463a852cc709e871c0d22dfaaad79 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Sat, 30 Nov 2024 18:05:52 +0100 Subject: [PATCH 1/2] Switch premiumMembership check to ext function --- .../net/primal/android/articles/feed/ArticleFeedViewModel.kt | 4 ++-- .../net/primal/android/notes/feed/list/NoteFeedViewModel.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedViewModel.kt b/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedViewModel.kt index 93346969..9bf62b0c 100644 --- a/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/articles/feed/ArticleFeedViewModel.kt @@ -17,6 +17,7 @@ import net.primal.android.articles.ArticleRepository import net.primal.android.articles.feed.ArticleFeedContract.UiState import net.primal.android.articles.feed.ui.mapAsFeedArticleUi import net.primal.android.feeds.domain.isPremiumFeedSpec +import net.primal.android.premium.utils.hasPremiumMembership import net.primal.android.user.accounts.active.ActiveAccountStore @HiltViewModel(assistedFactory = ArticleFeedViewModel.Factory::class) @@ -47,9 +48,8 @@ class ArticleFeedViewModel @AssistedInject constructor( private fun observeActiveAccount() { viewModelScope.launch { activeAccountStore.activeUserAccount.collect { - val hasPremiumMembership = it.premiumMembership?.isExpired() == false setState { - copy(paywall = spec.isPremiumFeedSpec() && !hasPremiumMembership) + copy(paywall = spec.isPremiumFeedSpec() && !it.hasPremiumMembership()) } } } diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt b/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt index 5820e606..c9902666 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/list/NoteFeedViewModel.kt @@ -39,6 +39,7 @@ import net.primal.android.notes.feed.list.NoteFeedContract.UiState import net.primal.android.notes.feed.model.FeedPostsSyncStats import net.primal.android.notes.feed.model.asFeedPostUi import net.primal.android.notes.repository.FeedRepository +import net.primal.android.premium.utils.hasPremiumMembership import net.primal.android.user.accounts.active.ActiveAccountStore import timber.log.Timber @@ -79,9 +80,8 @@ class NoteFeedViewModel @AssistedInject constructor( private fun observeActiveAccount() { viewModelScope.launch { activeAccountStore.activeUserAccount.collect { - val hasPremiumMembership = it.premiumMembership?.isExpired() == false setState { - copy(paywall = feedSpec.isPremiumFeedSpec() && !hasPremiumMembership) + copy(paywall = feedSpec.isPremiumFeedSpec() && !it.hasPremiumMembership()) } } } From 05432506b0b32bf41bdd5506495bcc41940c57e9 Mon Sep 17 00:00:00 2001 From: Marko Kocic Date: Sat, 30 Nov 2024 18:06:25 +0100 Subject: [PATCH 2/2] Implement paywall on MediaFeedGrid --- .../android/explore/feed/ExploreFeedScreen.kt | 1 + .../android/explore/home/ExploreHomeScreen.kt | 1 + .../notes/feed/grid/MediaFeedContract.kt | 1 + .../android/notes/feed/grid/MediaFeedGrid.kt | 16 +++++++++++++++- .../notes/feed/grid/MediaFeedViewModel.kt | 18 ++++++++++++++++++ .../android/premium/utils/PremiumUtils.kt | 6 ++++++ .../profile/details/ui/ProfileDetailsScreen.kt | 3 +++ 7 files changed, 45 insertions(+), 1 deletion(-) diff --git a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt index 7deb0c93..83c9e681 100644 --- a/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt +++ b/app/src/main/kotlin/net/primal/android/explore/feed/ExploreFeedScreen.kt @@ -229,6 +229,7 @@ private fun ExploreNoteFeed( feedSpec = feedSpec, contentPadding = contentPadding, onNoteClick = { noteCallbacks.onNoteClick?.invoke(it) }, + onGetPrimalPremiumClick = { noteCallbacks.onGetPrimalPremiumClick?.invoke() }, ) } } diff --git a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt index b5f9e6de..55eb490b 100644 --- a/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt +++ b/app/src/main/kotlin/net/primal/android/explore/home/ExploreHomeScreen.kt @@ -203,6 +203,7 @@ private fun ExploreHomeScreen( feedSpec = exploreMediaFeedSpec, contentPadding = paddingValues, onNoteClick = { noteCallbacks.onNoteClick?.invoke(it) }, + onGetPrimalPremiumClick = { noteCallbacks.onGetPrimalPremiumClick?.invoke() }, ) } diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedContract.kt b/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedContract.kt index 2c5a7027..d2a225e7 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedContract.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedContract.kt @@ -8,5 +8,6 @@ interface MediaFeedContract { data class UiState( val notes: Flow>, + val paywall: Boolean = false, ) } diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedGrid.kt b/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedGrid.kt index 3e4c799d..1ec25df3 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedGrid.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedGrid.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.rememberLazyGridState @@ -25,14 +26,19 @@ import androidx.paging.compose.itemKey import net.primal.android.R import net.primal.android.core.compose.GridLoadingPlaceholder import net.primal.android.core.compose.ListNoContent +import net.primal.android.core.compose.PremiumFeedPaywall import net.primal.android.core.compose.isEmpty import net.primal.android.notes.feed.model.FeedPostUi import timber.log.Timber +const val MAX_NOTES_BEFORE_PAYWALL = 25 +const val PAYWALL_COLUMN_SPAN = 3 + @Composable fun MediaFeedGrid( feedSpec: String, onNoteClick: (String) -> Unit, + onGetPrimalPremiumClick: () -> Unit, modifier: Modifier = Modifier, gridState: LazyGridState = rememberLazyGridState(), contentPadding: PaddingValues = PaddingValues(0.dp), @@ -49,6 +55,7 @@ fun MediaFeedGrid( MediaFeedGrid( state = uiState.value, onNoteClick = onNoteClick, + onGetPrimalPremiumClick = onGetPrimalPremiumClick, gridState = gridState, modifier = modifier, contentPadding = contentPadding, @@ -62,6 +69,7 @@ private fun MediaFeedGrid( modifier: Modifier = Modifier, state: MediaFeedContract.UiState, onNoteClick: (String) -> Unit, + onGetPrimalPremiumClick: () -> Unit, gridState: LazyGridState = rememberLazyGridState(), contentPadding: PaddingValues = PaddingValues(0.dp), noContentVerticalArrangement: Arrangement.Vertical = Arrangement.Center, @@ -86,7 +94,7 @@ private fun MediaFeedGrid( contentPadding = contentPadding, ) { items( - count = pagingItems.itemCount, + count = pagingItems.itemCount.ifPaywallCoerceAtMost(state.paywall), key = pagingItems.itemKey(key = { "${it.postId}${it.repostId}" }), contentType = pagingItems.itemContentType(), ) { index -> @@ -108,6 +116,9 @@ private fun MediaFeedGrid( else -> {} } } + item(span = { GridItemSpan(PAYWALL_COLUMN_SPAN) }) { + PremiumFeedPaywall(onClick = onGetPrimalPremiumClick) + } } } } @@ -153,3 +164,6 @@ private fun EmptyItemsContent( } } } + +private fun Int.ifPaywallCoerceAtMost(paywall: Boolean) = + run { if (paywall) this.coerceAtMost(MAX_NOTES_BEFORE_PAYWALL) else this } diff --git a/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedViewModel.kt b/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedViewModel.kt index 3ecbeb50..dc1bad7d 100644 --- a/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedViewModel.kt +++ b/app/src/main/kotlin/net/primal/android/notes/feed/grid/MediaFeedViewModel.kt @@ -10,15 +10,21 @@ import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.getAndUpdate import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import net.primal.android.feeds.domain.isPremiumFeedSpec import net.primal.android.notes.feed.grid.MediaFeedContract.UiState import net.primal.android.notes.feed.model.asFeedPostUi import net.primal.android.notes.repository.FeedRepository +import net.primal.android.premium.utils.hasPremiumMembership +import net.primal.android.user.accounts.active.ActiveAccountStore @HiltViewModel(assistedFactory = MediaFeedViewModel.Factory::class) class MediaFeedViewModel @AssistedInject constructor( @Assisted private val feedSpec: String, private val feedRepository: FeedRepository, + private val activeAccountStore: ActiveAccountStore, ) : ViewModel() { @AssistedFactory @@ -33,4 +39,16 @@ class MediaFeedViewModel @AssistedInject constructor( private val _state = MutableStateFlow(UiState(notes = buildFeedByDirective(feedSpec = feedSpec))) val state = _state.asStateFlow() + private fun setState(reducer: UiState.() -> UiState) = _state.getAndUpdate { it.reducer() } + + init { + observeActiveAccount() + } + + private fun observeActiveAccount() = + viewModelScope.launch { + activeAccountStore.activeUserAccount.collect { + setState { copy(paywall = feedSpec.isPremiumFeedSpec() && !it.hasPremiumMembership()) } + } + } } diff --git a/app/src/main/kotlin/net/primal/android/premium/utils/PremiumUtils.kt b/app/src/main/kotlin/net/primal/android/premium/utils/PremiumUtils.kt index 44206ffd..0ad2d70f 100644 --- a/app/src/main/kotlin/net/primal/android/premium/utils/PremiumUtils.kt +++ b/app/src/main/kotlin/net/primal/android/premium/utils/PremiumUtils.kt @@ -1,7 +1,13 @@ package net.primal.android.premium.utils +import net.primal.android.premium.domain.PremiumMembership +import net.primal.android.user.domain.UserAccount + fun String?.isPrimalLegend() = this?.lowercase() == "primal legend" fun String?.isPremiumFree() = this?.lowercase() == "free" fun String?.isOriginAndroid() = this?.lowercase() == "android" fun String?.isOriginIOS() = this?.lowercase() == "ios" fun String?.isOriginWeb() = this?.lowercase() == "web" + +fun PremiumMembership.hasPremiumMembership() = !this.isExpired() +fun UserAccount.hasPremiumMembership() = this.premiumMembership?.hasPremiumMembership() == true diff --git a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt index 11696b45..b2c92a73 100644 --- a/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt +++ b/app/src/main/kotlin/net/primal/android/profile/details/ui/ProfileDetailsScreen.kt @@ -523,6 +523,9 @@ fun ProfileDetailsScreen( onNoteClick = { naddr -> noteCallbacks.onNoteClick?.let { it(naddr) } }, noContentVerticalArrangement = Arrangement.Top, noContentPaddingValues = PaddingValues(top = 16.dp), + onGetPrimalPremiumClick = { + noteCallbacks.onGetPrimalPremiumClick?.invoke() + }, ) } }