diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt index ccaa3db61..b7b7ee6c8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppAction.kt @@ -8,7 +8,8 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import mozilla.components.lib.crash.Crash.NativeCodeCrash import mozilla.components.lib.state.Action -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory @@ -56,7 +57,7 @@ sealed class AppAction : Action { data class DisbandSearchGroupAction(val searchTerm: String) : AppAction() data class SelectPocketStoriesCategory(val categoryName: String) : AppAction() data class DeselectPocketStoriesCategory(val categoryName: String) : AppAction() - data class PocketStoriesShown(val storiesShown: List) : AppAction() + data class PocketStoriesShown(val storiesShown: List) : AppAction() data class PocketStoriesChange(val pocketStories: List) : AppAction() /** * Adds a set of items marked for removal to the app state, to be hidden in the UI. diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt index dcd88801a..8eaf79f19 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppState.kt @@ -9,7 +9,9 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import mozilla.components.lib.crash.Crash.NativeCodeCrash import mozilla.components.lib.state.State -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory @@ -39,6 +41,8 @@ import org.mozilla.fenix.gleanplumb.MessagingState * @property recentHistory The list of [RecentlyVisitedItem]s. * @property pocketStories The list of currently shown [PocketRecommendedStory]s. * @property pocketStoriesCategories All [PocketRecommendedStory] categories. + * @property pocketStoriesCategoriesSelections Current Pocket recommended stories categories selected by the user. + * @property pocketSponsoredStories All [PocketSponsoredStory]s. * @property messaging State related messages. * @property pendingDeletionHistoryItems The set of History items marked for removal in the UI, * awaiting to be removed once the Undo snackbar hides away. @@ -56,9 +60,10 @@ data class AppState( val recentSyncedTabState: RecentSyncedTabState = RecentSyncedTabState.None, val recentBookmarks: List = emptyList(), val recentHistory: List = emptyList(), - val pocketStories: List = emptyList(), + val pocketStories: List = emptyList(), val pocketStoriesCategories: List = emptyList(), val pocketStoriesCategoriesSelections: List = emptyList(), + val pocketSponsoredStories: List = emptyList(), val messaging: MessagingState = MessagingState(), val pendingDeletionHistoryItems: Set = emptySet(), ) : State diff --git a/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt b/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt index 2ec3cae6f..2deb0f140 100644 --- a/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt +++ b/app/src/main/java/org/mozilla/fenix/components/appstate/AppStoreReducer.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.components.appstate import androidx.annotation.VisibleForTesting +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.ext.filterOutTab import org.mozilla.fenix.ext.getFilteredStories @@ -141,10 +142,12 @@ internal object AppStoreReducer { pocketStories = updatedCategoriesState.getFilteredStories() ) } - is AppAction.PocketStoriesChange -> state.copy(pocketStories = action.pocketStories) + is AppAction.PocketStoriesChange -> state.copy( + pocketStories = action.pocketStories + ) is AppAction.PocketStoriesShown -> { var updatedCategories = state.pocketStoriesCategories - action.storiesShown.forEach { shownStory -> + action.storiesShown.filterIsInstance().forEach { shownStory -> updatedCategories = updatedCategories.map { category -> when (category.name == shownStory.category) { true -> { diff --git a/app/src/main/java/org/mozilla/fenix/ext/AppState.kt b/app/src/main/java/org/mozilla/fenix/ext/AppState.kt index 73b132c1e..ed8604060 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/AppState.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/AppState.kt @@ -5,11 +5,14 @@ package org.mozilla.fenix.ext import androidx.annotation.VisibleForTesting -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.home.blocklist.BlocklistHandler import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory +import org.mozilla.fenix.home.pocket.PocketStory import org.mozilla.fenix.home.recenttabs.RecentTab.SearchGroup @VisibleForTesting @@ -18,16 +21,21 @@ internal const val POCKET_STORIES_TO_SHOW_COUNT = 8 /** * Get the list of stories to be displayed based on the user selected categories. * - * @return a list of [PocketRecommendedStory]es from the currently selected categories. + * @return a list of [PocketStory]es from the currently selected categories. */ -fun AppState.getFilteredStories(): List { +fun AppState.getFilteredStories(): List { if (pocketStoriesCategoriesSelections.isEmpty()) { - return pocketStoriesCategories + val recommendedStories = pocketStoriesCategories .find { it.name == POCKET_STORIES_DEFAULT_CATEGORY_NAME }?.stories ?.sortedBy { it.timesShown } ?.take(POCKET_STORIES_TO_SHOW_COUNT) ?: emptyList() + + return combineRecommendedAndSponsoredStories( + recommendedStories = recommendedStories, + sponsoredStories = pocketSponsoredStories, + ) } val oldestSortedCategories = pocketStoriesCategoriesSelections @@ -48,6 +56,18 @@ fun AppState.getFilteredStories(): List { }.take(POCKET_STORIES_TO_SHOW_COUNT) } +private fun combineRecommendedAndSponsoredStories( + recommendedStories: List, + sponsoredStories: List, +): List { + val recommendedStoriesToShow = POCKET_STORIES_TO_SHOW_COUNT - sponsoredStories.size.coerceAtMost(2) + + return recommendedStories.take(1) + + sponsoredStories.take(1) + + recommendedStories.take(recommendedStoriesToShow).drop(1) + + sponsoredStories.take(2).drop(1) +} + /** * Get how many stories needs to be shown from each currently selected category. * diff --git a/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt b/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt index fb82ccdd8..9ae4b9f92 100644 --- a/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt @@ -14,8 +14,8 @@ import mozilla.components.lib.state.Action import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.Store -import mozilla.components.service.pocket.PocketRecommendedStory import mozilla.components.service.pocket.PocketStoriesService +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.components.appstate.AppState @@ -68,9 +68,11 @@ class PocketUpdatesMiddleware( persistStories( coroutineScope = coroutineScope, pocketStoriesService = pocketStoriesService, - updatedStories = action.storiesShown.map { - it.copy(timesShown = it.timesShown.inc()) - } + updatedStories = action.storiesShown + .filterIsInstance() + .map { + it.copy(timesShown = it.timesShown.inc()) + } ) } is AppAction.SelectPocketStoriesCategory, diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendedStoriesCategory.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendedStoriesCategory.kt index 8598306eb..bf9759234 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendedStoriesCategory.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketRecommendedStoriesCategory.kt @@ -4,7 +4,7 @@ package org.mozilla.fenix.home.pocket -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory /** * Category name of the default category from which stories are to be shown diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt index b07a7a9c5..f5783cae5 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt @@ -37,7 +37,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.R import org.mozilla.fenix.compose.ClickableSubstringLink import org.mozilla.fenix.compose.EagerFlingBehavior @@ -57,7 +58,7 @@ private const val POCKET_STORIES_UTM_VALUE = "pocket-newtab-android" private const val POCKET_FEATURE_UTM_KEY_VALUE = "utm_source=ff_android" /** - * Placeholder [PocketRecommendedStory] allowing to combine other items in the same list that shows stories. + * Placeholder [PocketStory] allowing to combine other items in the same list that shows stories. * It uses empty values for it's properties ensuring that no conflict is possible since real stories have * mandatory values. */ @@ -114,11 +115,11 @@ fun PocketStory( } /** - * Displays a list of [PocketRecommendedStory]es on 3 by 3 grid. + * Displays a list of [PocketStory]es on 3 by 3 grid. * If there aren't enough stories to fill all columns placeholders containing an external link * to go to Pocket for more recommendations are added. * - * @param stories The list of [PocketRecommendedStory]ies to be displayed. Expect a list with 8 items. + * @param stories The list of [PocketStory]ies to be displayed. Expect a list with 8 items. * @param contentPadding Dimension for padding the content after it has been clipped. * This space will be used for shadows and also content rendering when the list is scrolled. * @param onStoryClicked Callback for when the user taps on a recommended story. @@ -126,9 +127,9 @@ fun PocketStory( */ @Composable fun PocketStories( - @PreviewParameter(PocketStoryProvider::class) stories: List, + @PreviewParameter(PocketStoryProvider::class) stories: List, contentPadding: Dp, - onStoryClicked: (PocketRecommendedStory, Pair) -> Unit, + onStoryClicked: (PocketStory, Pair) -> Unit, onDiscoverMoreClicked: (String) -> Unit ) { // Show stories in at most 3 rows but on any number of columns depending on the data received. @@ -151,7 +152,7 @@ fun PocketStories( ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) { onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE") } - } else { + } else if (story is PocketRecommendedStory) { PocketStory(story) { val uri = Uri.parse(story.url) .buildUpon() @@ -284,12 +285,12 @@ private fun PocketStoriesComposablesPreview() { } } -private class PocketStoryProvider : PreviewParameterProvider { +private class PocketStoryProvider : PreviewParameterProvider { override val values = getFakePocketStories(7).asSequence() override val count = 8 } -internal fun getFakePocketStories(limit: Int = 1): List { +internal fun getFakePocketStories(limit: Int = 1): List { return mutableListOf().apply { for (index in 0 until limit) { add( diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt index 4d11085f4..489b843c2 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesController.kt @@ -7,7 +7,8 @@ package org.mozilla.fenix.home.pocket import androidx.annotation.VisibleForTesting import androidx.navigation.NavController import mozilla.components.service.glean.private.NoExtras -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Pocket import org.mozilla.fenix.HomeActivity @@ -16,15 +17,15 @@ import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction /** - * Contract for how all user interactions with the Pocket recommended stories feature are to be handled. + * Contract for how all user interactions with the Pocket stories feature are to be handled. */ interface PocketStoriesController { /** * Callback to decide what should happen as an effect of a new list of stories being shown. * - * @param storiesShown the new list of [PocketRecommendedStory]es shown to the user. + * @param storiesShown the new list of [PocketStory]es shown to the user. */ - fun handleStoriesShown(storiesShown: List) + fun handleStoriesShown(storiesShown: List) /** * Callback allowing to handle a specific [PocketRecommendedStoriesCategory] being clicked by the user. @@ -36,10 +37,10 @@ interface PocketStoriesController { /** * Callback for when the user clicks on a specific story. * - * @param storyClicked The just clicked [PocketRecommendedStory] URL. + * @param storyClicked The just clicked [PocketStory]. * @param storyPosition `row x column` matrix representing the grid position of the clicked story. */ - fun handleStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) + fun handleStoryClicked(storyClicked: PocketStory, storyPosition: Pair) /** * Callback for when the "Learn more" link is clicked. @@ -68,7 +69,7 @@ internal class DefaultPocketStoriesController( private val appStore: AppStore, private val navController: NavController, ) : PocketStoriesController { - override fun handleStoriesShown(storiesShown: List) { + override fun handleStoriesShown(storiesShown: List) { appStore.dispatch(AppAction.PocketStoriesShown(storiesShown)) Pocket.homeRecsShown.record(NoExtras()) } @@ -114,17 +115,19 @@ internal class DefaultPocketStoriesController( } override fun handleStoryClicked( - storyClicked: PocketRecommendedStory, + storyClicked: PocketStory, storyPosition: Pair ) { dismissSearchDialogIfDisplayed() homeActivity.openToBrowserAndLoad(storyClicked.url, true, BrowserDirection.FromHome) - Pocket.homeRecsStoryClicked.record( - Pocket.HomeRecsStoryClickedExtra( - position = "${storyPosition.first}x${storyPosition.second}", - timesShown = storyClicked.timesShown.inc().toString() + if (storyClicked is PocketRecommendedStory) { + Pocket.homeRecsStoryClicked.record( + Pocket.HomeRecsStoryClickedExtra( + position = "${storyPosition.first}x${storyPosition.second}", + timesShown = storyClicked.timesShown.inc().toString() + ) ) - ) + } } override fun handleLearnMoreClicked(link: String) { diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesInteractor.kt index 1dda23900..157ed0e13 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesInteractor.kt @@ -4,7 +4,8 @@ package org.mozilla.fenix.home.pocket -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory /** * Contract for all possible user interactions with the Pocket recommended stories feature. @@ -15,7 +16,7 @@ interface PocketStoriesInteractor { * * @param storiesShown The new list of [PocketRecommendedStory]es shown to the user. */ - fun onStoriesShown(storiesShown: List) + fun onStoriesShown(storiesShown: List) /** * Callback for when the user clicks a specific category. @@ -27,10 +28,10 @@ interface PocketStoriesInteractor { /** * Callback for when the user clicks on a specific story. * - * @param storyClicked The just clicked [PocketRecommendedStory] URL. + * @param storyClicked The just clicked [PocketStory]. * @param storyPosition `row x column` matrix representing the grid position of the clicked story. */ - fun onStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) + fun onStoryClicked(storyClicked: PocketStory, storyPosition: Pair) /** * Callback for when the user clicks the "Learn more" link. diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt index 585c20af9..3f1ff2c2c 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesViewHolder.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.RecyclerView import mozilla.components.lib.state.ext.observeAsComposableState -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.mozilla.fenix.R import org.mozilla.fenix.R.string import org.mozilla.fenix.components.components diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 2c8ade6b4..b67c5a5fb 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -7,7 +7,7 @@ package org.mozilla.fenix.home.sessioncontrol import mozilla.components.feature.tab.collections.Tab import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.gleanplumb.Message @@ -18,11 +18,11 @@ import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentbookmarks.controller.RecentBookmarksController import org.mozilla.fenix.home.recentbookmarks.interactor.RecentBookmarksInteractor import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab +import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController +import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recenttabs.controller.RecentTabController -import org.mozilla.fenix.home.recentsyncedtabs.controller.RecentSyncedTabController import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor -import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController @@ -435,7 +435,7 @@ class SessionControlInteractor( controller.handleCustomizeHomeTapped() } - override fun onStoriesShown(storiesShown: List) { + override fun onStoriesShown(storiesShown: List) { pocketStoriesController.handleStoriesShown(storiesShown) } @@ -443,7 +443,7 @@ class SessionControlInteractor( pocketStoriesController.handleCategoryClick(categoryClicked) } - override fun onStoryClicked(storyClicked: PocketRecommendedStory, storyPosition: Pair) { + override fun onStoryClicked(storyClicked: PocketStory, storyPosition: Pair) { pocketStoriesController.handleStoryClicked(storyClicked, storyPosition) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index f5b160236..a2b5f7566 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -11,11 +11,11 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory import org.mozilla.fenix.components.appstate.AppState -import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.gleanplumb.Message import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.OnboardingState import org.mozilla.fenix.home.recentbookmarks.RecentBookmark @@ -38,7 +38,7 @@ internal fun normalModeAdapterItems( nimbusMessageCard: Message? = null, recentTabs: List, recentVisits: List, - pocketStories: List + pocketStories: List ): List { val items = mutableListOf() var shouldShowCustomizeHome = false diff --git a/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt b/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt index 9c98b6c06..07f0e8b47 100644 --- a/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/AppStoreTest.kt @@ -14,7 +14,8 @@ import mozilla.components.concept.sync.DeviceType import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.fxa.manager.FxaAccountManager -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull @@ -288,7 +289,7 @@ class AppStoreTest { fun `Test selecting a Pocket recommendations category`() = runTest { val otherStoriesCategory = PocketRecommendedStoriesCategory("other") val anotherStoriesCategory = PocketRecommendedStoriesCategory("another") - val filteredStories = listOf(mockk()) + val filteredStories = listOf(mockk()) appStore = AppStore( AppState( pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory), @@ -316,7 +317,7 @@ class AppStoreTest { fun `Test deselecting a Pocket recommendations category`() = runTest { val otherStoriesCategory = PocketRecommendedStoriesCategory("other") val anotherStoriesCategory = PocketRecommendedStoriesCategory("another") - val filteredStories = listOf(mockk()) + val filteredStories = listOf(mockk()) appStore = AppStore( AppState( pocketStoriesCategories = listOf(otherStoriesCategory, anotherStoriesCategory), @@ -363,7 +364,7 @@ class AppStoreTest { appStore = AppStore(AppState()) mockkStatic("org.mozilla.fenix.ext.AppStateKt") { - val firstFilteredStories = listOf(mockk()) + val firstFilteredStories = listOf(mockk()) every { any().getFilteredStories() } returns firstFilteredStories appStore.dispatch( @@ -378,7 +379,7 @@ class AppStoreTest { assertSame(firstFilteredStories, appStore.state.pocketStories) val updatedCategories = listOf(PocketRecommendedStoriesCategory("yetAnother")) - val secondFilteredStories = listOf(mockk()) + val secondFilteredStories = listOf(mockk()) every { any().getFilteredStories() } returns secondFilteredStories appStore.dispatch( AppAction.PocketStoriesCategoriesChange( @@ -399,7 +400,7 @@ class AppStoreTest { appStore = AppStore(AppState()) mockkStatic("org.mozilla.fenix.ext.AppStateKt") { - val firstFilteredStories = listOf(mockk()) + val firstFilteredStories = listOf(mockk()) every { any().getFilteredStories() } returns firstFilteredStories appStore.dispatch( diff --git a/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt b/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt index 7a911623e..4c33234ae 100644 --- a/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt +++ b/app/src/test/java/org/mozilla/fenix/ext/AppStateTest.kt @@ -5,7 +5,9 @@ package org.mozilla.fenix.ext import io.mockk.mockk -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory +import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertSame @@ -29,7 +31,7 @@ class AppStateTest { ) @Test - fun `GIVEN no category is selected WHEN getFilteredStories is called THEN only Pocket stories from the default category are returned`() { + fun `GIVEN no category is selected and no sponsored stories are available WHEN getFilteredStories is called THEN only Pocket stories from the default category are returned`() { val state = AppState( pocketStoriesCategories = listOf( otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategory @@ -38,11 +40,15 @@ class AppStateTest { val result = state.getFilteredStories() - assertNull(result.firstOrNull { it.category != POCKET_STORIES_DEFAULT_CATEGORY_NAME }) + assertNull( + result.firstOrNull { + it is PocketRecommendedStory && it.category != POCKET_STORIES_DEFAULT_CATEGORY_NAME + } + ) } @Test - fun `GIVEN no category is selected WHEN getFilteredStories is called THEN no more than the default stories number are returned from the default category`() { + fun `GIVEN no category is selected and no sponsored stories are available WHEN getFilteredStories is called THEN no more than the default stories number are returned from the default category`() { val defaultStoriesCategoryWithManyStories = PocketRecommendedStoriesCategory( POCKET_STORIES_DEFAULT_CATEGORY_NAME, getFakePocketStories(POCKET_STORIES_TO_SHOW_COUNT + 2) @@ -58,6 +64,63 @@ class AppStateTest { assertEquals(POCKET_STORIES_TO_SHOW_COUNT, result.size) } + @Test + fun `GIVEN no category is selected and 1 sponsored story available WHEN getFilteredStories is called THEN get stories from the default category combined with the sponsored one`() { + val defaultStoriesCategoryWithManyStories = PocketRecommendedStoriesCategory( + POCKET_STORIES_DEFAULT_CATEGORY_NAME, + getFakePocketStories(POCKET_STORIES_TO_SHOW_COUNT) + ) + val sponsoredStories = getFakeSponsoredStories(1) + val state = AppState( + pocketStoriesCategories = listOf( + otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategoryWithManyStories + ), + pocketSponsoredStories = sponsoredStories + ) + + val result = state.getFilteredStories().toMutableList() + + assertEquals(POCKET_STORIES_TO_SHOW_COUNT, result.size) + assertEquals(sponsoredStories[0], result[1]) // second story should be a sponsored one + result.removeAt(1) // remove the sponsored story to hopefully only remain with general recommendations + assertNull( + result.firstOrNull { + it is PocketRecommendedStory && it.category != POCKET_STORIES_DEFAULT_CATEGORY_NAME + } + ) + } + + @Test + fun `GIVEN no category is selected and 2 sponsored stories available WHEN getFilteredStories is called THEN get stories from the default category combined with the sponsored stories`() { + val defaultStoriesCategoryWithManyStories = PocketRecommendedStoriesCategory( + POCKET_STORIES_DEFAULT_CATEGORY_NAME, + getFakePocketStories(POCKET_STORIES_TO_SHOW_COUNT) + ) + val sponsoredStories = getFakeSponsoredStories(2) + val state = AppState( + pocketStoriesCategories = listOf( + otherStoriesCategory, anotherStoriesCategory, defaultStoriesCategoryWithManyStories + ), + pocketSponsoredStories = sponsoredStories + ) + + val result = state.getFilteredStories().toMutableList() + + assertEquals(POCKET_STORIES_TO_SHOW_COUNT, result.size) + // second story should be a sponsored one + assertEquals(sponsoredStories[0], result[1]) + // last story should be a sponsored one + assertEquals(sponsoredStories[1], result[POCKET_STORIES_TO_SHOW_COUNT - 1]) + // remove the sponsored stories to hopefully only remain with general recommendations + result.removeAt(7) + result.removeAt(1) + assertNull( + result.firstOrNull { + it is PocketRecommendedStory && it.category != POCKET_STORIES_DEFAULT_CATEGORY_NAME + } + ) + } + @Test fun `GIVEN a category is selected WHEN getFilteredStories is called THEN only stories from that category are returned`() { val state = AppState( @@ -67,7 +130,11 @@ class AppStateTest { val result = state.getFilteredStories() - assertNull(result.firstOrNull { it.category != otherStoriesCategory.name }) + assertNull( + result.firstOrNull { + it is PocketRecommendedStory && it.category != otherStoriesCategory.name + } + ) } @Test @@ -103,7 +170,9 @@ class AppStateTest { assertEquals(6, result.size) assertNull( result.firstOrNull { - it.category != otherStoriesCategory.name && it.category != anotherStoriesCategory.name + it is PocketRecommendedStory && + it.category != otherStoriesCategory.name && + it.category != anotherStoriesCategory.name } ) } @@ -262,7 +331,11 @@ class AppStateTest { val result = state.getFilteredStories() assertEquals(3, result.size) - assertNull(result.firstOrNull { it.category != anotherStoriesCategory.name }) + assertNull( + result.firstOrNull { + it is PocketRecommendedStory && it.category != anotherStoriesCategory.name + } + ) } @Test @@ -306,3 +379,20 @@ private fun getFakePocketStories( } } } + +private fun getFakeSponsoredStories(limit: Int) = mutableListOf().apply { + for (index in 0 until limit) { + add( + PocketSponsoredStory( + title = "Story title $index", + url = "https://sponsored.story", + imageUrl = "https://sponsored.image", + sponsor = "Sponsor $index", + shim = PocketSponsoredStoryShim( + click = "Story title $index click shim", + impression = "Story title $index impression shim" + ) + ) + ) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/home/PocketUpdatesMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/home/PocketUpdatesMiddlewareTest.kt index 6fb2b8042..7dabcabad 100644 --- a/app/src/test/java/org/mozilla/fenix/home/PocketUpdatesMiddlewareTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/PocketUpdatesMiddlewareTest.kt @@ -13,8 +13,8 @@ import io.mockk.verify import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf -import mozilla.components.service.pocket.PocketRecommendedStory import mozilla.components.service.pocket.PocketStoriesService +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.rule.MainCoroutineRule import mozilla.components.support.test.rule.runTestOnMain @@ -34,7 +34,9 @@ class PocketUpdatesMiddlewareTest { @Test fun `WHEN PocketStoriesShown is dispatched THEN update PocketStoriesService`() = runTestOnMain { - val story1 = PocketRecommendedStory("title", "url1", "imageUrl", "publisher", "category", 0, timesShown = 0) + val story1 = PocketRecommendedStory( + "title", "url1", "imageUrl", "publisher", "category", 0, timesShown = 0 + ) val story2 = story1.copy("title2", "url2") val story3 = story1.copy("title3", "url3") val pocketService: PocketStoriesService = mockk(relaxed = true) diff --git a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt index b4ffd1b2a..d9682caa7 100644 --- a/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/SessionControlInteractorTest.kt @@ -9,7 +9,7 @@ import io.mockk.mockk import io.mockk.verify import mozilla.components.feature.tab.collections.Tab import mozilla.components.feature.tab.collections.TabCollection -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory import org.junit.Before import org.junit.Test import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -239,7 +239,7 @@ class SessionControlInteractorTest { @Test fun `GIVEN a PocketStoriesInteractor WHEN stories are shown THEN handle it in a PocketStoriesController`() { - val shownStories: List = mockk() + val shownStories: List = mockk() interactor.onStoriesShown(shownStories) @@ -257,7 +257,7 @@ class SessionControlInteractorTest { @Test fun `GIVEN a PocketStoriesInteractor WHEN a story is clicked THEN handle it in a PocketStoriesController`() { - val clickedStory: PocketRecommendedStory = mockk() + val clickedStory: PocketStory = mockk() val storyGridLocation = 1 to 2 interactor.onStoryClicked(clickedStory, storyGridLocation) diff --git a/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt index 1a7dc4f1c..711fcb6bf 100644 --- a/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/pocket/DefaultPocketStoriesControllerTest.kt @@ -10,7 +10,9 @@ import io.mockk.mockk import io.mockk.spyk import io.mockk.verify import io.mockk.verifyOrder -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import mozilla.components.support.test.robolectric.testContext import mozilla.telemetry.glean.testing.GleanTestRule import org.junit.Assert.assertEquals @@ -146,7 +148,7 @@ class DefaultPocketStoriesControllerTest { fun `WHEN new stories are shown THEN update the State and record telemetry`() { val store = spyk(AppStore()) val controller = DefaultPocketStoriesController(mockk(), store, mockk()) - val storiesShown: List = mockk() + val storiesShown: List = mockk() assertFalse(Pocket.homeRecsShown.testHasValue()) controller.handleStoriesShown(storiesShown) @@ -158,7 +160,7 @@ class DefaultPocketStoriesControllerTest { } @Test - fun `WHEN a story is clicked then open that story's url using HomeActivity and record telemetry`() { + fun `WHEN a recommended story is clicked THEN open that story's url using HomeActivity and record telemetry`() { val story = PocketRecommendedStory( title = "", url = "testLink", @@ -185,6 +187,25 @@ class DefaultPocketStoriesControllerTest { assertEquals(story.timesShown.inc().toString(), event.single().extra!!["times_shown"]) } + @Test + fun `WHEN a sponsored story is clicked THEN open that story's url using HomeActivity and don't record telemetry`() { + val story = PocketSponsoredStory( + title = "", + url = "testLink", + imageUrl = "", + sponsor = "", + shim = mockk() + ) + val homeActivity: HomeActivity = mockk(relaxed = true) + val controller = DefaultPocketStoriesController(homeActivity, mockk(), mockk(relaxed = true)) + assertFalse(Pocket.homeRecsStoryClicked.testHasValue()) + + controller.handleStoryClicked(story, 1 to 2) + + verify { homeActivity.openToBrowserAndLoad(story.url, true, BrowserDirection.FromHome) } + assertFalse(Pocket.homeRecsStoryClicked.testHasValue()) + } + @Test fun `WHEN discover more is clicked then open that using HomeActivity and record telemetry`() { val link = "http://getpocket.com/explore" diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt index 62da50c05..8ca614f4e 100644 --- a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/SessionControlViewTest.kt @@ -10,7 +10,8 @@ import io.mockk.mockk import io.mockk.verify import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite -import mozilla.components.service.pocket.PocketRecommendedStory +import mozilla.components.service.pocket.PocketStory +import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -67,24 +68,24 @@ class SessionControlViewTest { @Test fun `GIVEN pocketArticles WHEN calling shouldShowHomeOnboardingDialog THEN show the dialog `() { - val pocketArticles = listOf(PocketRecommendedStory("", "", "", "", "", 0, 0)) + val pocketStories = listOf(PocketRecommendedStory("", "", "", "", "", 0, 0)) val settings: Settings = mockk() every { settings.hasShownHomeOnboardingDialog } returns false - val state = AppState(pocketStories = pocketArticles) + val state = AppState(pocketStories = pocketStories) assertTrue(state.shouldShowHomeOnboardingDialog(settings)) } @Test fun `GIVEN the home onboading dialog has been shown before WHEN calling shouldShowHomeOnboardingDialog THEN DO NOT showthe dialog `() { - val pocketArticles = listOf(PocketRecommendedStory("", "", "", "", "", 0, 0)) + val pocketStories = listOf(PocketRecommendedStory("", "", "", "", "", 0, 0)) val settings: Settings = mockk() every { settings.hasShownHomeOnboardingDialog } returns true - val state = AppState(pocketStories = pocketArticles) + val state = AppState(pocketStories = pocketStories) assertFalse(state.shouldShowHomeOnboardingDialog(settings)) } @@ -139,7 +140,7 @@ class SessionControlViewTest { val recentBookmarks = listOf(RecentBookmark()) val recentTabs = emptyList() val historyMetadata = emptyList() - val pocketArticles = emptyList() + val pocketStories = emptyList() every { settings.showTopSitesFeature } returns true every { settings.showRecentTabsFeature } returns true @@ -157,7 +158,7 @@ class SessionControlViewTest { null, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertTrue(results[0] is AdapterItem.TopPlaceholderItem) @@ -175,7 +176,7 @@ class SessionControlViewTest { val recentBookmarks = listOf(RecentBookmark()) val recentTabs = emptyList() val historyMetadata = emptyList() - val pocketArticles = emptyList() + val pocketStories = emptyList() val nimbusMessageCard: Message = mockk() every { settings.showTopSitesFeature } returns true @@ -194,7 +195,7 @@ class SessionControlViewTest { nimbusMessageCard, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertTrue(results.contains(AdapterItem.NimbusMessageCard(nimbusMessageCard))) @@ -209,7 +210,7 @@ class SessionControlViewTest { val recentBookmarks = listOf() val recentTabs = listOf(mockk()) val historyMetadata = emptyList() - val pocketArticles = emptyList() + val pocketStories = emptyList() every { settings.showTopSitesFeature } returns true every { settings.showRecentTabsFeature } returns true @@ -227,7 +228,7 @@ class SessionControlViewTest { null, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertTrue(results[0] is AdapterItem.TopPlaceholderItem) @@ -245,7 +246,7 @@ class SessionControlViewTest { val recentBookmarks = listOf() val recentTabs = emptyList() val historyMetadata = listOf(RecentHistoryGroup("title", emptyList())) - val pocketArticles = emptyList() + val pocketStories = emptyList() every { settings.showTopSitesFeature } returns true every { settings.showRecentTabsFeature } returns true @@ -263,7 +264,7 @@ class SessionControlViewTest { null, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertTrue(results[0] is AdapterItem.TopPlaceholderItem) @@ -281,7 +282,7 @@ class SessionControlViewTest { val recentBookmarks = listOf() val recentTabs = emptyList() val historyMetadata = emptyList() - val pocketArticles = listOf(PocketRecommendedStory("", "", "", "", "", 1, 1)) + val pocketStories = listOf(PocketRecommendedStory("", "", "", "", "", 1, 1)) every { settings.showTopSitesFeature } returns true every { settings.showRecentTabsFeature } returns true @@ -299,7 +300,7 @@ class SessionControlViewTest { null, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertTrue(results[0] is AdapterItem.TopPlaceholderItem) @@ -318,7 +319,7 @@ class SessionControlViewTest { val recentBookmarks = listOf() val recentTabs = emptyList() val historyMetadata = emptyList() - val pocketArticles = emptyList() + val pocketStories = emptyList() every { settings.showTopSitesFeature } returns true every { settings.showRecentTabsFeature } returns true @@ -336,7 +337,7 @@ class SessionControlViewTest { null, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertEquals(results.size, 2) assertTrue(results[0] is AdapterItem.TopPlaceholderItem) @@ -354,7 +355,7 @@ class SessionControlViewTest { val recentBookmarks = listOf(mockk()) val recentTabs = listOf(mockk()) val historyMetadata = listOf(mockk()) - val pocketArticles = listOf(mockk()) + val pocketStories = listOf(mockk()) every { settings.showTopSitesFeature } returns true every { settings.showRecentTabsFeature } returns true @@ -372,7 +373,7 @@ class SessionControlViewTest { null, recentTabs, historyMetadata, - pocketArticles + pocketStories ) assertTrue(results[0] is AdapterItem.TopPlaceholderItem)