tor-browser

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

commit bd2fa00c5373a1e8bce6f230ac88024680648829
parent 3157db2a5b406bd617543b88f1edd5457cab81f9
Author: Devota Aabel <daabel@mozilla.com>
Date:   Thu,  9 Oct 2025 16:38:11 +0000

Bug 1988271- Update number of columns on the Stories Discover More page to display based on different window size classes r=android-reviewers,007

Differential Revision: https://phabricator.services.mozilla.com/D265168

Diffstat:
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/ListItemTabSurface.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt | 21+++++++++++++++++----
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoryCard.kt | 43+++++++++++++++++++++++++++++--------------
3 files changed, 93 insertions(+), 18 deletions(-)

diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/ListItemTabSurface.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/compose/ListItemTabSurface.kt @@ -4,9 +4,16 @@ package org.mozilla.fenix.compose +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues @@ -22,9 +29,11 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.PreviewLightDark @@ -86,6 +95,9 @@ fun ListItemTabSurface( private = false, targetSize = imageWidth, contentScale = imageContentScale, + placeholder = { + Box(modifier = Modifier.size(IMAGE_SIZE.dp).skeletonLoader()) + }, ) Spacer(Modifier.width(FirefoxTheme.layout.space.static100)) @@ -131,3 +143,38 @@ private fun ListItemTabSurfaceWithCustomBackgroundPreview() { } } } + +/** + * Applies a shimmering skeleton loading effect to the current [Modifier]. + * + * This can be used as a placeholder for UI elements while their content is loading. + * + * @param durationMillis The duration in milliseconds of the shimmer animation cycle. + * Defaults to `1000`. + * @param color1 The starting color of the gradient animation. Defaults to [Color.LightGray]. + * @param color2 The ending color of the gradient animation. Defaults to [Color.White]. + * + * @return A [Modifier] that displays a skeleton loader effect. + */ +@Composable +fun Modifier.skeletonLoader( + durationMillis: Int = 1000, + color1: Color = Color.LightGray, + color2: Color = Color.White, +): Modifier { + val transition = rememberInfiniteTransition(label = "") + + val color by transition.animateColor( + initialValue = color1, + targetValue = color2, + animationSpec = infiniteRepeatable( + animation = tween(durationMillis, easing = LinearEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "", + ) + + return drawBehind { + drawRect(color = color) + } +} diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoriesScreen.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import mozilla.components.compose.base.annotation.FlexibleWindowLightDarkPreview +import mozilla.components.compose.base.theme.layout.AcornWindowSize import mozilla.components.compose.base.utils.BackInvokedHandler import org.mozilla.fenix.R import org.mozilla.fenix.components.appstate.recommendations.ContentRecommendationsState @@ -116,14 +117,26 @@ private fun Stories( state: ContentRecommendationsState, interactor: PocketStoriesInteractor, ) { + val windowSizeClass = FirefoxTheme.windowSize + val columnCount = when (windowSizeClass) { + AcornWindowSize.Small -> 1 + AcornWindowSize.Medium -> 2 + AcornWindowSize.Large -> 3 + } + + val verticalPadding = if (windowSizeClass != AcornWindowSize.Small) { + 16.dp + } else { + 12.dp + } + LazyVerticalGrid( - columns = GridCells.Fixed(1), - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp), + columns = GridCells.Fixed(columnCount), + verticalArrangement = Arrangement.spacedBy(verticalPadding), + horizontalArrangement = Arrangement.spacedBy(16.dp, alignment = Alignment.CenterHorizontally), ) { itemsIndexed(state.pocketStories) { index, story -> StoryCard( - modifier = Modifier.padding(horizontal = 16.dp), story = story, onClick = interactor::onStoryClicked, ) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoryCard.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/pocket/ui/StoryCard.kt @@ -5,11 +5,12 @@ package org.mozilla.fenix.home.pocket.ui import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape @@ -33,13 +34,14 @@ import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory import mozilla.components.service.pocket.PocketStory.SponsoredContent import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.Image +import org.mozilla.fenix.compose.skeletonLoader import org.mozilla.fenix.home.fake.FakeHomepagePreview import org.mozilla.fenix.theme.FirefoxTheme import kotlin.math.roundToInt private val cardShape = RoundedCornerShape(8.dp) private val defaultCardContentPadding = 8.dp -private val imageSize = 345.dp +private val imageWidth = 345.dp private val imageHeight = 180.dp @OptIn(ExperimentalMaterial3Api::class) @@ -49,6 +51,15 @@ internal fun StoryCard( onClick: (story: PocketStory, position: Triple<Int, Int, Int>) -> Unit, modifier: Modifier = Modifier, ) { + val imageUrl = story.imageUrl.replace( + "{wh}", + with(LocalDensity.current) { + "${imageWidth.toPx().roundToInt()}x${ + imageWidth.toPx().roundToInt() + }" + }, + ) + Card( onClick = { onClick(story, Triple(0, 0, 0)) @@ -61,24 +72,18 @@ internal fun StoryCard( Column( modifier = Modifier.padding(all = defaultCardContentPadding), ) { - val imageUrl = story.imageUrl.replace( - "{wh}", - with(LocalDensity.current) { - "${imageSize.toPx().roundToInt()}x${ - imageSize.toPx().roundToInt() - }" - }, - ) - Image( url = imageUrl, modifier = Modifier .fillMaxWidth() - .height(imageHeight) + .aspectRatio(imageWidth / imageHeight) .clip(cardShape), private = false, - targetSize = imageSize, + targetSize = imageWidth, contentScale = ContentScale.Crop, + placeholder = { + Placeholder() + }, ) Column( @@ -89,6 +94,7 @@ internal fun StoryCard( text = story.title, color = FirefoxTheme.colors.textPrimary, overflow = TextOverflow.Ellipsis, + maxLines = 2, style = FirefoxTheme.typography.headline7, ) @@ -104,7 +110,7 @@ internal fun StoryCard( is PocketRecommendedStory, is PocketSponsoredStory, - -> { + -> { // no-op, don't handle these [PocketStory] types as they are no longer // supported after the Merino recommendation migration. } @@ -129,6 +135,15 @@ internal fun StoryCard( } } +@Composable +private fun Placeholder() { + Box( + modifier = Modifier + .aspectRatio(imageWidth / imageHeight) + .skeletonLoader(), + ) +} + @PreviewLightDark @Composable private fun StoryCardPreview() {