commit 4d2d26e5da95b506d0efb14c9828052a807f392c
parent f6140c866888a5c89e0f97b05ad843b5a93896b6
Author: Devota Aabel <daabel@mozilla.com>
Date: Fri, 14 Nov 2025 16:11:40 +0000
Bug 1998740- Pin stories to bottom of homepage when the simplified homescreen is active. r=gl,android-reviewers
Updated bottom padding on homepage to be dynamic based on whether the toolbar is at the top or bottom. When the minimal layout is active and only stories OR only stories and top sites are active, then a weighted spacer is added above the stories so that it becomes anchored to the bottom of the screen, with nothing but the new dynamic padding below it.
Differential Revision: https://phabricator.services.mozilla.com/D271697
Diffstat:
4 files changed, 298 insertions(+), 153 deletions(-)
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/Stories.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/Stories.kt
@@ -431,7 +431,7 @@ fun Stories(
@Suppress("MagicNumber")
@Composable
@Preview
-private fun StoriesPreview() {
+private fun StoriesWithCategoriesPreview() {
FirefoxTheme {
Box(
Modifier
@@ -469,6 +469,36 @@ private fun StoriesPreview() {
}
}
+@Suppress("MagicNumber")
+@Composable
+@Preview
+private fun StoriesPreview() {
+ FirefoxTheme {
+ Box(
+ Modifier
+ .background(MaterialTheme.colorScheme.surface)
+ .systemBarsPadding()
+ .padding(top = 32.dp),
+ ) {
+ Stories(
+ stories = listOf(
+ FakeHomepagePreview.pocketRecommendedStory(15),
+ FakeHomepagePreview.pocketSponsoredStory(15),
+ FakeHomepagePreview.contentRecommendation(15),
+ FakeHomepagePreview.sponsoredContent(15),
+ FakeHomepagePreview.pocketRecommendedStory(1),
+ FakeHomepagePreview.pocketSponsoredStory(1),
+ FakeHomepagePreview.contentRecommendation(1),
+ FakeHomepagePreview.sponsoredContent(1),
+ ),
+ contentPadding = 0.dp,
+ onStoryShown = { _, _ -> },
+ onStoryClicked = { _, _ -> },
+ )
+ }
+ }
+}
+
@Composable
@ReadOnlyComposable
private fun maxLines() = if (limitMaxLines()) DEFAULT_MAX_LINES else Int.MAX_VALUE
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/HomepageState.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/HomepageState.kt
@@ -91,6 +91,7 @@ internal sealed class HomepageState {
* @property buttonBackgroundColor Background [Color] for buttons.
* @property buttonTextColor Text [Color] for buttons.
* @property isSearchInProgress Whether search is currently active on the homepage.
+ * @property bottomPadding Amount of padding to display at the bottom of the homepage.
*/
internal data class Normal(
val nimbusMessage: NimbusMessageState?,
@@ -118,6 +119,7 @@ internal sealed class HomepageState {
val buttonBackgroundColor: Color,
val buttonTextColor: Color,
override val isSearchInProgress: Boolean,
+ val bottomPadding: Int,
) : HomepageState()
val browsingMode: BrowsingMode
@@ -126,7 +128,21 @@ internal sealed class HomepageState {
is Private -> BrowsingMode.Private
}
+ /**
+ * Returns whether the homepage is in the "Minimal Layout" state, where only the shortcuts and
+ * stories are visible (but both or either can be hidden). This is for the purpose of adding a
+ * weighted spacer in between so the stories are anchored to the bottom.
+ */
+ internal fun isMinimalLayout(): Boolean {
+ return (this as? Normal)?.run {
+ !showRecentTabs && !showRecentSyncedTab && !showBookmarks && !showRecentlyVisited &&
+ (!showCollections || collectionsState == CollectionsState.Gone) && !showHeader
+ } ?: false
+ }
+
companion object {
+ private const val BOTTOM_PADDING_TOP_TOOLBAR = 68
+ private const val BOTTOM_PADDING_BOTTOM_TOOLBAR = 32
/**
* Builds a new [HomepageState] from the current [AppState] and [Settings].
@@ -143,56 +159,97 @@ internal sealed class HomepageState {
): HomepageState {
return with(appState) {
if (browsingModeManager.mode.isPrivate) {
- Private(
- showHeader = settings.showHomepageHeader,
- firstFrameDrawn = firstFrameDrawn,
- isSearchInProgress = searchState.isSearchActive,
- privateModeRedesignEnabled = settings.enablePrivateBrowsingModeRedesign,
+ buildPrivateState(
+ appState = appState,
+ settings = settings,
)
} else {
- Normal(
- nimbusMessage = NimbusMessageState.build(appState),
- topSites = topSites,
- recentTabs = recentTabs,
- syncedTab = when (recentSyncedTabState) {
- RecentSyncedTabState.None,
- RecentSyncedTabState.Loading,
- -> null
-
- is RecentSyncedTabState.Success -> recentSyncedTabState.tabs.firstOrNull()
- },
- bookmarks = bookmarks,
- recentlyVisited = recentHistory,
- collectionsState = CollectionsState.build(
- appState = appState,
- browserState = components.core.store.state,
- browsingModeManager = browsingModeManager,
- ),
- pocketState = PocketState.build(appState = appState, settings = settings),
- showTopSites = settings.showTopSitesFeature && topSites.isNotEmpty(),
- showRecentTabs = shouldShowRecentTabs(settings),
- showBookmarks = settings.showBookmarksHomeFeature && bookmarks.isNotEmpty(),
- showRecentSyncedTab = shouldShowRecentSyncedTabs() && settings.showSyncedTabs,
- showRecentlyVisited = settings.historyMetadataUIFeature && recentHistory.isNotEmpty(),
- showPocketStories = settings.showPocketRecommendationsFeature &&
- recommendationState.pocketStories.isNotEmpty(),
- showCollections = settings.collections,
- showHeader = settings.showHomepageHeader,
- searchBarVisible = shouldShowSearchBar(appState = appState),
- searchBarEnabled = settings.enableHomepageSearchBar &&
- settings.toolbarPosition == ToolbarPosition.TOP &&
- LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
- firstFrameDrawn = firstFrameDrawn,
- setupChecklistState = setupChecklistState,
- topSiteColors = TopSiteColors.colors(wallpaperState = wallpaperState),
- cardBackgroundColor = wallpaperState.cardBackgroundColor,
- buttonBackgroundColor = wallpaperState.buttonBackgroundColor,
- buttonTextColor = wallpaperState.buttonTextColor,
- isSearchInProgress = searchState.isSearchActive,
+ buildNormalState(
+ appState = appState,
+ browsingModeManager = browsingModeManager,
+ settings = settings,
)
}
}
}
+
+ /**
+ * Builds a new [HomepageState.Private] from the current [AppState] and [Settings].
+ *
+ * @param appState State to build the [HomepageState.Private] from.
+ * @param settings [Settings] corresponding to how the homepage should be displayed.
+ */
+ private fun buildPrivateState(
+ appState: AppState,
+ settings: Settings,
+ ) = with(appState) {
+ Private(
+ showHeader = settings.showHomepageHeader,
+ firstFrameDrawn = firstFrameDrawn,
+ isSearchInProgress = searchState.isSearchActive,
+ privateModeRedesignEnabled = settings.enablePrivateBrowsingModeRedesign,
+ )
+ }
+
+ /**
+ * Builds a new [HomepageState.Normal] from the current [AppState] and [Settings].
+ *
+ * @param appState State to build the [HomepageState.Normal] from.
+ * @param browsingModeManager Manager holding current state of whether the browser is in private mode or not.
+ * @param settings [Settings] corresponding to how the homepage should be displayed.
+ */
+ @Composable
+ private fun buildNormalState(
+ appState: AppState,
+ browsingModeManager: BrowsingModeManager,
+ settings: Settings,
+ ) = with(appState) {
+ Normal(
+ nimbusMessage = NimbusMessageState.build(appState),
+ topSites = topSites,
+ recentTabs = recentTabs,
+ syncedTab = when (recentSyncedTabState) {
+ RecentSyncedTabState.None,
+ RecentSyncedTabState.Loading,
+ -> null
+
+ is RecentSyncedTabState.Success -> recentSyncedTabState.tabs.firstOrNull()
+ },
+ bookmarks = bookmarks,
+ recentlyVisited = recentHistory,
+ collectionsState = CollectionsState.build(
+ appState = appState,
+ browserState = components.core.store.state,
+ browsingModeManager = browsingModeManager,
+ ),
+ pocketState = PocketState.build(appState = appState, settings = settings),
+ showTopSites = settings.showTopSitesFeature && topSites.isNotEmpty(),
+ showRecentTabs = shouldShowRecentTabs(settings),
+ showBookmarks = settings.showBookmarksHomeFeature && bookmarks.isNotEmpty(),
+ showRecentSyncedTab = shouldShowRecentSyncedTabs() && settings.showSyncedTabs,
+ showRecentlyVisited = settings.historyMetadataUIFeature && recentHistory.isNotEmpty(),
+ showPocketStories = settings.showPocketRecommendationsFeature &&
+ recommendationState.pocketStories.isNotEmpty(),
+ showCollections = settings.collections,
+ showHeader = settings.showHomepageHeader,
+ searchBarVisible = shouldShowSearchBar(appState = appState),
+ searchBarEnabled = settings.enableHomepageSearchBar &&
+ settings.toolbarPosition == ToolbarPosition.TOP &&
+ LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT,
+ firstFrameDrawn = firstFrameDrawn,
+ setupChecklistState = setupChecklistState,
+ topSiteColors = TopSiteColors.colors(wallpaperState = wallpaperState),
+ cardBackgroundColor = wallpaperState.cardBackgroundColor,
+ buttonBackgroundColor = wallpaperState.buttonBackgroundColor,
+ buttonTextColor = wallpaperState.buttonTextColor,
+ isSearchInProgress = searchState.isSearchActive,
+ bottomPadding = if (settings.toolbarPosition == ToolbarPosition.TOP) {
+ BOTTOM_PADDING_TOP_TOOLBAR
+ } else {
+ BOTTOM_PADDING_BOTTOM_TOOLBAR
+ },
+ )
+ }
}
}
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/Homepage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/Homepage.kt
@@ -6,6 +6,7 @@ package org.mozilla.fenix.home.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@@ -17,6 +18,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.ReadOnlyComposable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
@@ -73,8 +75,6 @@ import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.utils.isLargeScreenSize
import org.mozilla.fenix.wallpapers.WallpaperState
-private const val BOTTOM_PADDING = 47
-
/**
* Top level composable for the homepage.
*
@@ -93,136 +93,149 @@ internal fun Homepage(
) {
val scrollState = rememberScrollState()
- Column(
+ BoxWithConstraints(
modifier = modifier
- .semantics {
- testTagsAsResourceId = true
- testTag = HOMEPAGE
- }
- .pointerInput(state.isSearchInProgress) {
- if (state.isSearchInProgress) {
- awaitPointerEventScope {
- interactor.onHomeContentFocusedWhileSearchIsActive()
+ .fillMaxSize(),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .semantics {
+ testTagsAsResourceId = true
+ testTag = HOMEPAGE
+ }
+ .pointerInput(state.isSearchInProgress) {
+ if (state.isSearchInProgress) {
+ awaitPointerEventScope {
+ interactor.onHomeContentFocusedWhileSearchIsActive()
+ }
}
}
+ .verticalScroll(scrollState),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ if (state.showHeader) {
+ HomepageHeader(
+ browsingMode = state.browsingMode,
+ browsingModeChanged = interactor::onPrivateModeButtonClicked,
+ )
+ } else {
+ Spacer(modifier = Modifier.height(16.dp))
}
- .verticalScroll(scrollState),
- ) {
- if (state.showHeader) {
- HomepageHeader(
- browsingMode = state.browsingMode,
- browsingModeChanged = interactor::onPrivateModeButtonClicked,
- )
- } else {
- Spacer(modifier = Modifier.height(16.dp))
- }
- if (state.firstFrameDrawn) {
- with(state) {
- when (this) {
- is HomepageState.Private -> {
- if (privateModeRedesignEnabled) {
- PrivateBrowsingDescription2(
- onLearnMoreClick = interactor::onLearnMoreClicked,
- )
- } else {
- Box(modifier = Modifier.padding(horizontal = horizontalMargin)) {
- PrivateBrowsingDescription(
+ if (state.firstFrameDrawn) {
+ with(state) {
+ when (this) {
+ is HomepageState.Private -> {
+ if (privateModeRedesignEnabled) {
+ PrivateBrowsingDescription2(
onLearnMoreClick = interactor::onLearnMoreClicked,
)
+ } else {
+ Box(modifier = Modifier.padding(horizontal = horizontalMargin)) {
+ PrivateBrowsingDescription(
+ onLearnMoreClick = interactor::onLearnMoreClicked,
+ )
+ }
}
}
- }
- is HomepageState.Normal -> {
- nimbusMessage?.let {
- NimbusMessageCardSection(
- nimbusMessage = nimbusMessage,
- interactor = interactor,
- )
- }
+ is HomepageState.Normal -> {
+ nimbusMessage?.let {
+ NimbusMessageCardSection(
+ nimbusMessage = nimbusMessage,
+ interactor = interactor,
+ )
+ }
- if (showTopSites) {
- TopSitesSection(
- topSites = topSites,
- topSiteColors = topSiteColors,
- interactor = interactor,
- onTopSitesItemBound = onTopSitesItemBound,
- )
- }
+ if (showTopSites) {
+ TopSitesSection(
+ topSites = topSites,
+ topSiteColors = topSiteColors,
+ interactor = interactor,
+ onTopSitesItemBound = onTopSitesItemBound,
+ )
+ }
- MaybeAddSetupChecklist(setupChecklistState, interactor)
-
- if (showRecentTabs) {
- RecentTabsSection(
- interactor = interactor,
- cardBackgroundColor = cardBackgroundColor,
- recentTabs = recentTabs,
- )
-
- if (showRecentSyncedTab) {
- Box(
- modifier = Modifier.padding(
- start = horizontalMargin,
- end = horizontalMargin,
- top = verticalMargin,
- ),
- ) {
- RecentSyncedTab(
- tab = syncedTab,
- backgroundColor = cardBackgroundColor,
- buttonBackgroundColor = if (syncedTab != null) {
- buttonBackgroundColor
- } else {
- FirefoxTheme.colors.layer3
- },
- buttonTextColor = buttonTextColor,
- onRecentSyncedTabClick = interactor::onRecentSyncedTabClicked,
- onSeeAllSyncedTabsButtonClick = interactor::onSyncedTabShowAllClicked,
- onRemoveSyncedTab = interactor::onRemovedRecentSyncedTab,
- )
+ MaybeAddSetupChecklist(setupChecklistState, interactor)
+
+ if (showRecentTabs) {
+ RecentTabsSection(
+ interactor = interactor,
+ cardBackgroundColor = cardBackgroundColor,
+ recentTabs = recentTabs,
+ )
+
+ if (showRecentSyncedTab) {
+ Box(
+ modifier = Modifier.padding(
+ start = horizontalMargin,
+ end = horizontalMargin,
+ top = verticalMargin,
+ ),
+ ) {
+ RecentSyncedTab(
+ tab = syncedTab,
+ backgroundColor = cardBackgroundColor,
+ buttonBackgroundColor = if (syncedTab != null) {
+ buttonBackgroundColor
+ } else {
+ FirefoxTheme.colors.layer3
+ },
+ buttonTextColor = buttonTextColor,
+ onRecentSyncedTabClick = interactor::onRecentSyncedTabClicked,
+ onSeeAllSyncedTabsButtonClick = interactor::onSyncedTabShowAllClicked,
+ onRemoveSyncedTab = interactor::onRemovedRecentSyncedTab,
+ )
+ }
}
}
- }
- if (showBookmarks) {
- BookmarksSection(
- bookmarks = bookmarks,
- cardBackgroundColor = cardBackgroundColor,
- interactor = interactor,
- )
- }
+ if (showBookmarks) {
+ BookmarksSection(
+ bookmarks = bookmarks,
+ cardBackgroundColor = cardBackgroundColor,
+ interactor = interactor,
+ )
+ }
- if (showRecentlyVisited) {
- RecentlyVisitedSection(
- recentVisits = recentlyVisited,
- cardBackgroundColor = cardBackgroundColor,
- interactor = interactor,
- )
- }
+ if (showRecentlyVisited) {
+ RecentlyVisitedSection(
+ recentVisits = recentlyVisited,
+ cardBackgroundColor = cardBackgroundColor,
+ interactor = interactor,
+ )
+ }
- if (showCollections) {
- CollectionsSection(
- collectionsState = collectionsState,
- interactor = interactor,
- )
- }
+ if (showCollections) {
+ CollectionsSection(
+ collectionsState = collectionsState,
+ interactor = interactor,
+ )
+ }
- if (showPocketStories) {
- Spacer(Modifier.padding(top = 72.dp))
+ if (showPocketStories) {
+ Spacer(
+ modifier = if (isMinimalLayout()) {
+ Modifier.weight(1f)
+ } else {
+ Modifier.padding(top = 72.dp)
+ },
+ )
- PocketSection(
- state = pocketState,
- cardBackgroundColor = cardBackgroundColor,
- interactor = interactor,
- )
+ PocketSection(
+ state = pocketState,
+ cardBackgroundColor = cardBackgroundColor,
+ interactor = interactor,
+ )
+ }
+
+ Spacer(Modifier.height(bottomPadding.dp))
}
}
}
}
}
-
- Spacer(Modifier.height(BOTTOM_PADDING.dp))
}
}
@@ -476,6 +489,7 @@ private fun HomepagePreview() {
buttonTextColor = WallpaperState.default.buttonTextColor,
buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor,
isSearchInProgress = false,
+ bottomPadding = 68,
),
interactor = FakeHomepagePreview.homepageInteractor,
onTopSitesItemBound = {},
@@ -517,6 +531,49 @@ private fun HomepagePreviewCollections() {
buttonTextColor = WallpaperState.default.buttonTextColor,
buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor,
isSearchInProgress = false,
+ bottomPadding = 68,
+ ),
+ interactor = FakeHomepagePreview.homepageInteractor,
+ onTopSitesItemBound = {},
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color = MaterialTheme.colorScheme.surface),
+ )
+ }
+}
+
+@Composable
+@PreviewLightDark
+private fun MinimalHomepagePreview() {
+ FirefoxTheme {
+ Homepage(
+ state = HomepageState.Normal(
+ nimbusMessage = null,
+ topSites = FakeHomepagePreview.topSites(),
+ recentTabs = FakeHomepagePreview.recentTabs(),
+ syncedTab = FakeHomepagePreview.recentSyncedTab(),
+ bookmarks = FakeHomepagePreview.bookmarks(),
+ recentlyVisited = FakeHomepagePreview.recentHistory(),
+ collectionsState = FakeHomepagePreview.collectionState(),
+ pocketState = FakeHomepagePreview.pocketState(),
+ showTopSites = true,
+ showRecentTabs = false,
+ showRecentSyncedTab = false,
+ showBookmarks = false,
+ showRecentlyVisited = false,
+ showPocketStories = true,
+ showCollections = false,
+ showHeader = false,
+ searchBarVisible = false,
+ searchBarEnabled = false,
+ firstFrameDrawn = true,
+ setupChecklistState = null,
+ topSiteColors = TopSiteColors.colors(),
+ cardBackgroundColor = WallpaperState.default.cardBackgroundColor,
+ buttonTextColor = WallpaperState.default.buttonTextColor,
+ buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor,
+ isSearchInProgress = false,
+ bottomPadding = 68,
),
interactor = FakeHomepagePreview.homepageInteractor,
onTopSitesItemBound = {},
diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/MiddleSearchHomepage.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/MiddleSearchHomepage.kt
@@ -184,6 +184,7 @@ private fun MiddleSearchHomepagePreview() {
buttonTextColor = WallpaperState.default.buttonTextColor,
buttonBackgroundColor = WallpaperState.default.buttonBackgroundColor,
isSearchInProgress = false,
+ bottomPadding = 68,
),
interactor = FakeHomepagePreview.homepageInteractor,
onTopSitesItemBound = {},