tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/Stories.kt | 32+++++++++++++++++++++++++++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/store/HomepageState.kt | 145+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/Homepage.kt | 273++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/ui/MiddleSearchHomepage.kt | 1+
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 = {},