Bug 1923477: Migrated pocket section to compose homepage. r=gl,android-reviewers

Differential Revision: https://phabricator.services.mozilla.com/D224989
This commit is contained in:
Devota Aabel 2024-11-13 20:30:30 +00:00
parent b1345af3c1
commit 26adbca329
5 changed files with 315 additions and 39 deletions

View File

@ -5,6 +5,7 @@
package org.mozilla.fenix.home.fake
import android.content.Context
import androidx.compose.runtime.Composable
import com.google.firebase.util.nextAlphanumericString
import mozilla.components.browser.state.state.ContentState
import mozilla.components.browser.state.state.TabSessionState
@ -16,13 +17,19 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryCaps
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.compose.SelectableChipColors
import org.mozilla.fenix.home.bookmarks.Bookmark
import org.mozilla.fenix.home.bookmarks.interactor.BookmarksInteractor
import org.mozilla.fenix.home.collections.CollectionsState
import org.mozilla.fenix.home.interactor.HomepageInteractor
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketState
import org.mozilla.fenix.home.privatebrowsing.interactor.PrivateBrowsingInteractor
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recentsyncedtabs.interactor.RecentSyncedTabInteractor
@ -35,6 +42,7 @@ import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor
import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.wallpapers.WallpaperState
import java.io.File
import kotlin.random.Random
@ -69,13 +77,19 @@ internal object FakeHomepagePreview {
override fun openCustomizeHomePage() { /* no op */ }
override fun onStoryShown(storyShown: PocketStory, storyPosition: Pair<Int, Int>) { /* no op */ }
override fun onStoryShown(
storyShown: PocketStory,
storyPosition: Pair<Int, Int>,
) { /* no op */ }
override fun onStoriesShown(storiesShown: List<PocketStory>) { /* no op */ }
override fun onCategoryClicked(categoryClicked: PocketRecommendedStoriesCategory) { /* no op */ }
override fun onStoryClicked(storyClicked: PocketStory, storyPosition: Pair<Int, Int>) { /* no op */ }
override fun onStoryClicked(
storyClicked: PocketStory,
storyPosition: Pair<Int, Int>,
) { /* no op */ }
override fun onLearnMoreClicked(link: String) { /* no op */ }
@ -168,7 +182,10 @@ internal object FakeHomepagePreview {
override fun onRenameCollectionTapped(collection: TabCollection) { /* no op */ }
override fun onToggleCollectionExpanded(collection: TabCollection, expand: Boolean) { /* no op */ }
override fun onToggleCollectionExpanded(
collection: TabCollection,
expand: Boolean,
) { /* no op */ }
override fun onAddTabsToCollectionTapped() { /* no op */ }
@ -326,6 +343,51 @@ internal object FakeHomepagePreview {
}
}
@Composable
internal fun pocketState(limit: Int = 1) = PocketState(
stories = mutableListOf<PocketStory>().apply {
for (index in 0 until limit) {
when (index % 2 == 0) {
true -> add(
PocketRecommendedStory(
title = "This is a ${"very ".repeat(index)} long title",
publisher = "Publisher",
url = "https://story$index.com",
imageUrl = "",
timeToRead = index,
category = "Category #$index",
timesShown = index.toLong(),
),
)
false -> add(
PocketSponsoredStory(
id = index,
title = "This is a ${"very ".repeat(index)} long title",
url = "https://sponsored-story$index.com",
imageUrl = "",
sponsor = "Mozilla",
shim = PocketSponsoredStoryShim("", ""),
priority = index,
caps = PocketSponsoredStoryCaps(
flightCount = index,
flightPeriod = index * 2,
lifetimeCount = index * 3,
),
),
)
}
}
},
categories = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
.split(" ")
.map { PocketRecommendedStoriesCategory(it) },
categoriesSelections = emptyList(),
categoryColors = SelectableChipColors.buildColors(),
textColor = FirefoxTheme.colors.textPrimary,
linkTextColor = FirefoxTheme.colors.textAccent,
)
private const val URL = "mozilla.com"
private fun randomLong() = random.nextLong()

View File

@ -0,0 +1,108 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.pocket
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.home.HomeSectionHeader
import org.mozilla.fenix.home.fake.FakeHomepagePreview
import org.mozilla.fenix.home.pocket.interactor.PocketStoriesInteractor
import org.mozilla.fenix.home.pocket.ui.PocketStories
import org.mozilla.fenix.home.pocket.ui.PocketStoriesCategories
import org.mozilla.fenix.home.pocket.ui.PoweredByPocketHeader
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.wallpapers.WallpaperState
/**
* Pocket section for the homepage.
*
* @param horizontalPadding Horizontal padding to apply to outermost column.
* @param state The [PocketState] representing the UI state.
* @param cardBackgroundColor The [Color] of the card backgrounds.
* @param interactor [PocketStoriesInteractor] for interactions with the UI.
*/
@Composable
fun PocketSection(
horizontalPadding: Dp = dimensionResource(R.dimen.home_item_horizontal_margin),
state: PocketState,
cardBackgroundColor: Color,
interactor: PocketStoriesInteractor,
) {
Column(modifier = Modifier.padding(top = 72.dp)) {
// Simple wrapper to add horizontal padding to just the header while the stories have none.
Box(modifier = Modifier.padding(horizontal = horizontalPadding)) {
HomeSectionHeader(
headerText = stringResource(R.string.pocket_stories_header_1),
)
}
Spacer(Modifier.height(16.dp))
PocketStories(
stories = state.stories,
contentPadding = horizontalPadding,
backgroundColor = cardBackgroundColor,
onStoryShown = interactor::onStoryShown,
onStoryClicked = interactor::onStoryClicked,
onDiscoverMoreClicked = interactor::onDiscoverMoreClicked,
)
Spacer(Modifier.height(24.dp))
HomeSectionHeader(
headerText = stringResource(R.string.pocket_stories_categories_header),
)
Spacer(Modifier.height(16.dp))
if (state.categories.isNotEmpty()) {
PocketStoriesCategories(
categories = state.categories,
selections = state.categoriesSelections,
modifier = Modifier.fillMaxWidth(),
categoryColors = state.categoryColors,
onCategoryClick = interactor::onCategoryClicked,
)
}
Spacer(Modifier.height(24.dp))
PoweredByPocketHeader(
onLearnMoreClicked = interactor::onLearnMoreClicked,
modifier = Modifier.fillMaxWidth(),
textColor = state.textColor,
linkTextColor = state.linkTextColor,
)
}
}
@Preview
@Composable
private fun PocketSectionPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) {
PocketSection(
horizontalPadding = 0.dp,
state = FakeHomepagePreview.pocketState(),
cardBackgroundColor = WallpaperState.default.cardBackgroundColor,
interactor = FakeHomepagePreview.homepageInteractor,
)
}
}
}

View File

@ -0,0 +1,93 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.pocket
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import mozilla.components.service.pocket.PocketStory
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.compose.SelectableChipColors
import org.mozilla.fenix.theme.FirefoxTheme
/**
* State object that describes the pocket section of the homepage.
*
* @property stories List of [PocketStory] to display.
* @property categories List of [PocketRecommendedStoriesCategory] to display.
* @property categoriesSelections List of selectable [PocketRecommendedStoriesSelectedCategory] to display.
* @property categoryColors Color parameters for the selectable categories.
* @property textColor [Color] for text.
* @property linkTextColor [Color] for link text.
*/
data class PocketState(
val stories: List<PocketStory>,
val categories: List<PocketRecommendedStoriesCategory> = emptyList(),
val categoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList(),
val categoryColors: SelectableChipColors,
val textColor: Color,
val linkTextColor: Color,
) {
/**
* Companion object for building [PocketState].
*/
companion object {
/**
* Builds a new [PocketState] from the current [AppState].
*
* @param appState State to build the [PocketState] from.
*/
@Composable
internal fun build(appState: AppState) = with(appState) {
var textColor = FirefoxTheme.colors.textPrimary
var linkTextColor = FirefoxTheme.colors.textAccent
wallpaperState.currentWallpaper.let { currentWallpaper ->
currentWallpaper.textColor?.let {
val wallpaperAdaptedTextColor = Color(it)
textColor = wallpaperAdaptedTextColor
linkTextColor = wallpaperAdaptedTextColor
}
}
PocketState(
stories = pocketStories,
categories = pocketStoriesCategories,
categoriesSelections = pocketStoriesCategoriesSelections,
categoryColors = getSelectableChipColors(),
textColor = textColor,
linkTextColor = linkTextColor,
)
}
}
}
@Composable
private fun AppState.getSelectableChipColors(): SelectableChipColors {
var (selectedBackgroundColor, unselectedBackgroundColor, selectedTextColor, unselectedTextColor) =
SelectableChipColors.buildColors()
wallpaperState.composeRunIfWallpaperCardColorsAreAvailable { cardColorLight, cardColorDark ->
if (isSystemInDarkTheme()) {
selectedBackgroundColor = cardColorDark
unselectedBackgroundColor = cardColorLight
selectedTextColor = FirefoxTheme.colors.textActionPrimary
unselectedTextColor = FirefoxTheme.colors.textActionSecondary
} else {
selectedBackgroundColor = cardColorLight
unselectedBackgroundColor = cardColorDark
selectedTextColor = FirefoxTheme.colors.textActionSecondary
unselectedTextColor = FirefoxTheme.colors.textActionPrimary
}
}
return SelectableChipColors(
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
)
}

View File

@ -13,6 +13,7 @@ import org.mozilla.fenix.ext.shouldShowRecentSyncedTabs
import org.mozilla.fenix.ext.shouldShowRecentTabs
import org.mozilla.fenix.home.bookmarks.Bookmark
import org.mozilla.fenix.home.collections.CollectionsState
import org.mozilla.fenix.home.pocket.PocketState
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTabState
import org.mozilla.fenix.home.recenttabs.RecentTab
@ -43,11 +44,13 @@ internal sealed class HomepageState {
* @property bookmarks List of [Bookmark] to display.
* @property recentlyVisited List of [RecentlyVisitedItem] to display.
* @property collectionsState State of the collections section to display.
* @property pocketState State of the pocket section to display.
* @property showTopSites Whether to show top sites or not.
* @property showRecentTabs Whether to show recent tabs or not.
* @property showRecentSyncedTab Whether to show recent synced tab or not.
* @property showBookmarks Whether to show bookmarks.
* @property showRecentlyVisited Whether to show recent history section.
* @property showPocketStories Whether to show the pocket stories section.
* @property topSiteColors The color set defined by [TopSiteColors] used to style a top site.
* @property cardBackgroundColor Background color for card items.
* @property buttonBackgroundColor Background [Color] for buttons.
@ -60,11 +63,13 @@ internal sealed class HomepageState {
val bookmarks: List<Bookmark>,
val recentlyVisited: List<RecentlyVisitedItem>,
val collectionsState: CollectionsState,
val pocketState: PocketState,
val showTopSites: Boolean,
val showRecentTabs: Boolean,
val showRecentSyncedTab: Boolean,
val showBookmarks: Boolean,
val showRecentlyVisited: Boolean,
val showPocketStories: Boolean,
val topSiteColors: TopSiteColors,
val cardBackgroundColor: Color,
val buttonBackgroundColor: Color,
@ -105,11 +110,13 @@ internal sealed class HomepageState {
appState = appState,
browserState = components.core.store.state,
),
pocketState = PocketState.build(appState),
showTopSites = settings.showTopSitesFeature && topSites.isNotEmpty(),
showRecentTabs = shouldShowRecentTabs(settings),
showBookmarks = settings.showBookmarksHomeFeature && bookmarks.isNotEmpty(),
showRecentSyncedTab = shouldShowRecentSyncedTabs(),
showRecentlyVisited = settings.historyMetadataUIFeature && recentHistory.isNotEmpty(),
showPocketStories = settings.showPocketRecommendationsFeature && pocketStories.isNotEmpty(),
topSiteColors = TopSiteColors.colors(wallpaperState = wallpaperState),
cardBackgroundColor = wallpaperState.cardBackgroundColor,
buttonBackgroundColor = wallpaperState.buttonBackgroundColor,

View File

@ -17,12 +17,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.GleanMetrics.RecentlyVisitedHomepage
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.home.HomeSectionHeader
import org.mozilla.fenix.home.bookmarks.Bookmark
import org.mozilla.fenix.home.bookmarks.interactor.BookmarksInteractor
@ -32,6 +32,7 @@ import org.mozilla.fenix.home.collections.Collections
import org.mozilla.fenix.home.collections.CollectionsState
import org.mozilla.fenix.home.fake.FakeHomepagePreview
import org.mozilla.fenix.home.interactor.HomepageInteractor
import org.mozilla.fenix.home.pocket.PocketSection
import org.mozilla.fenix.home.recentsyncedtabs.view.RecentSyncedTab
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor
@ -50,6 +51,7 @@ import org.mozilla.fenix.home.store.HomepageState
import org.mozilla.fenix.home.topsites.TopSiteColors
import org.mozilla.fenix.home.topsites.TopSites
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.WallpaperState
/**
@ -59,7 +61,7 @@ import org.mozilla.fenix.wallpapers.WallpaperState
* @param interactor for interactions with the homepage UI.
* @param onTopSitesItemBound Invoked during the composition of a top site item.
*/
@Suppress("LongParameterList")
@Suppress("LongMethod")
@Composable
internal fun Homepage(
state: HomepageState,
@ -67,7 +69,8 @@ internal fun Homepage(
onTopSitesItemBound: () -> Unit,
) {
Column(
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState()),
) {
with(state) {
@ -133,6 +136,15 @@ internal fun Homepage(
}
CollectionsSection(collectionsState = collectionsState, interactor = interactor)
if (showPocketStories) {
PocketSection(
state = pocketState,
cardBackgroundColor = cardBackgroundColor,
interactor = interactor,
)
}
// This is a temporary value until I can fix layout issues
Spacer(Modifier.height(288.dp))
}
@ -237,6 +249,7 @@ private fun RecentlyVisitedSection(
RecentlyVisitedHomepage.historyHighlightOpened.record(NoExtras())
interactor.onRecentHistoryHighlightClicked(recentlyVisitedItem)
}
is RecentHistoryGroup -> {
RecentlyVisitedHomepage.searchGroupOpened.record(NoExtras())
History.recentSearchesTapped.record(
@ -289,46 +302,39 @@ private fun CollectionsPlaceholder() {
}
@Composable
@LightDarkPreview
@Preview
private fun HomepagePreview() {
FirefoxTheme {
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxSize()
.background(color = FirefoxTheme.colors.layer1)
.verticalScroll(scrollState),
) {
Homepage(
HomepageState.Normal(
showTopSites = true,
topSiteColors = TopSiteColors.colors(),
topSites = FakeHomepagePreview.topSites(),
showRecentTabs = true,
recentTabs = FakeHomepagePreview.recentTabs(),
cardBackgroundColor = WallpaperState.default.cardBackgroundColor,
buttonTextColor = WallpaperState.default.buttonTextColor,
buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor,
showRecentSyncedTab = true,
syncedTab = FakeHomepagePreview.recentSyncedTab(),
showBookmarks = true,
bookmarks = FakeHomepagePreview.bookmarks(),
showRecentlyVisited = true,
recentlyVisited = FakeHomepagePreview.recentHistory(),
collectionsState = FakeHomepagePreview.collectionState(),
),
interactor = FakeHomepagePreview.homepageInteractor,
onTopSitesItemBound = {},
)
}
Homepage(
HomepageState.Normal(
topSites = FakeHomepagePreview.topSites(),
recentTabs = FakeHomepagePreview.recentTabs(),
syncedTab = FakeHomepagePreview.recentSyncedTab(),
bookmarks = FakeHomepagePreview.bookmarks(),
recentlyVisited = FakeHomepagePreview.recentHistory(),
collectionsState = FakeHomepagePreview.collectionState(),
pocketState = FakeHomepagePreview.pocketState(),
showTopSites = true,
showRecentTabs = true,
showRecentSyncedTab = true,
showBookmarks = true,
showRecentlyVisited = true,
showPocketStories = true,
topSiteColors = TopSiteColors.colors(),
cardBackgroundColor = WallpaperState.default.cardBackgroundColor,
buttonTextColor = WallpaperState.default.buttonTextColor,
buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor,
),
interactor = FakeHomepagePreview.homepageInteractor,
onTopSitesItemBound = {},
)
}
}
@Composable
@LightDarkPreview
@Preview
private fun PrivateHomepagePreview() {
FirefoxTheme {
FirefoxTheme(theme = Theme.Private) {
Box(
modifier = Modifier
.fillMaxSize()