diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt index bbf87af618..fc6489542d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt @@ -48,6 +48,21 @@ sealed class RecentTab { */ data class Tab(val state: TabSessionState) : RecentTab() + /** + * A tab that was recently viewed on a synced device. + * + * @param deviceDisplayName The device the tab was viewed on. + * @param title The title of the tab. + * @param url The url of the tab. + * @param previewImageUrl The url used to retrieve the preview image of the tab. + */ + data class SyncedTab( + val deviceDisplayName: String, + val title: String, + val url: String, + val previewImageUrl: String?, + ) : RecentTab() + /** * A search term group that was recently viewed * diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt index 6b35a3472a..c702abf7a0 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabViewHolder.kt @@ -46,7 +46,7 @@ class RecentTabViewHolder( RecentTabs( recentTabs = recentTabs.value ?: emptyList(), onRecentTabClick = { interactor.onRecentTabClicked(it) }, - onRecentSearchGroupClicked = { interactor.onRecentSearchGroupClicked(it) }, + onRecentSearchGroupClick = { interactor.onRecentSearchGroupClicked(it) }, menuItems = listOf( RecentTabMenuItem( title = stringResource(id = R.string.recent_tab_menu_item_remove), diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt index 660a89759e..06fa4bd676 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt @@ -2,7 +2,7 @@ * 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/. */ -@file:Suppress("MagicNumber") +@file:Suppress("MagicNumber", "TooManyFunctions") package org.mozilla.fenix.home.recenttabs.view @@ -16,6 +16,7 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight @@ -26,6 +27,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Card import androidx.compose.material.DropdownMenu import androidx.compose.material.DropdownMenuItem @@ -50,6 +53,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -59,6 +63,7 @@ import mozilla.components.browser.icons.compose.WithIcon import mozilla.components.concept.base.images.ImageLoadRequest import mozilla.components.support.ktx.kotlin.getRepresentativeSnippet import mozilla.components.ui.colors.PhotonColors +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.Image @@ -71,14 +76,19 @@ import org.mozilla.fenix.theme.FirefoxTheme * @param recentTabs List of [RecentTab] to display. * @param menuItems List of [RecentTabMenuItem] shown long clicking a [RecentTab]. * @param onRecentTabClick Invoked when the user clicks on a recent tab. - * @param onRecentSearchGroupClicked Invoked when the user clicks on a recent search group. + * @param onRecentSearchGroupClick Invoked when the user clicks on a recent search group. + * @param onRecentSyncedTabClick Invoked when the user clicks on the recent synced tab. + * @param onSyncedTabSeeAllButtonClick Invoked when user clicks on the "See all" button in the synced tab card. */ @Composable +@Suppress("LongParameterList") fun RecentTabs( recentTabs: List, menuItems: List, onRecentTabClick: (String) -> Unit = {}, - onRecentSearchGroupClicked: (String) -> Unit = {} + onRecentSearchGroupClick: (String) -> Unit = {}, + onRecentSyncedTabClick: (RecentTab.SyncedTab) -> Unit = {}, + onSyncedTabSeeAllButtonClick: () -> Unit = {}, ) { Column( modifier = Modifier.fillMaxWidth(), @@ -99,7 +109,16 @@ fun RecentTabs( searchTerm = tab.searchTerm, tabId = tab.tabId, count = tab.count, - onSearchGroupClicked = onRecentSearchGroupClicked + onSearchGroupClick = onRecentSearchGroupClick + ) + } + } + is RecentTab.SyncedTab -> { + if (FeatureFlags.taskContinuityFeature) { + RecentSyncedTabItem( + tab, + onRecentSyncedTabClick, + onSyncedTabSeeAllButtonClick, ) } } @@ -158,9 +177,7 @@ private fun RecentTabItem( Row { RecentTabIcon( url = tab.state.content.url, - modifier = Modifier - .size(18.dp, 18.dp) - .clip(RoundedCornerShape(2.dp)), + modifier = Modifier.size(18.dp).clip(RoundedCornerShape(2.dp)), icon = tab.state.content.icon ) @@ -186,7 +203,7 @@ private fun RecentTabItem( * @param searchTerm The search term for the group. * @param tabId The id of the last accessed tab in the group. * @param count Count of how many tabs belongs to the group. - * @param onSearchGroupClicked Invoked when the user clicks on a group. + * @param onSearchGroupClick Invoked when the user clicks on a group. */ @Suppress("LongParameterList") @Composable @@ -194,13 +211,13 @@ private fun RecentSearchGroupItem( searchTerm: String, tabId: String, count: Int, - onSearchGroupClicked: (String) -> Unit = {} + onSearchGroupClick: (String) -> Unit = {} ) { Card( modifier = Modifier .fillMaxWidth() .height(112.dp) - .clickable { onSearchGroupClicked(tabId) }, + .clickable { onSearchGroupClick(tabId) }, shape = RoundedCornerShape(8.dp), backgroundColor = FirefoxTheme.colors.layer2, elevation = 6.dp @@ -244,25 +261,139 @@ private fun RecentSearchGroupItem( } } +/** + * A recent synced tab. + * + * @param tab Optional synced tab. If null, displays placeholders. + * @param onRecentSyncedTabClick Invoked when item is clicked. + * @param onSeeAllButtonClick Invoked when "See all" button is clicked. + */ +@Suppress("LongMethod") +@Composable +private fun RecentSyncedTabItem( + tab: RecentTab.SyncedTab?, + onRecentSyncedTabClick: (RecentTab.SyncedTab) -> Unit, + onSeeAllButtonClick: () -> Unit, +) { + Card( + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + .clickable { tab?.let { onRecentSyncedTabClick(tab) } }, + shape = RoundedCornerShape(8.dp), + backgroundColor = FirefoxTheme.colors.layer2, + elevation = 6.dp + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row(modifier = Modifier.height(IntrinsicSize.Min)) { + if (tab == null) { + RecentTabImagePlaceholder() + } else { + RecentTabImage( + tab = tab, + modifier = Modifier + .size(108.dp, 80.dp) + .clip(RoundedCornerShape(8.dp)) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxHeight() + ) { + if (tab == null) { + RecentTabTitlePlaceholder() + } else { + RecentTabTitle(title = tab.title) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + if (tab == null) { + Box( + modifier = Modifier + .background(FirefoxTheme.colors.layer3) + .size(18.dp) + ) + } else { + Image( + painter = painterResource(R.drawable.ic_synced_tabs), + contentDescription = stringResource( + R.string.recent_tabs_synced_device_icon_content_description + ), + modifier = Modifier.size(18.dp, 18.dp) + ) + } + + Spacer(modifier = Modifier.width(8.dp)) + + if (tab == null) { + TextLinePlaceHolder() + } else { + RecentTabSubtitle(subtitle = tab.deviceDisplayName) + } + } + } + } + + Spacer(modifier = Modifier.height(32.dp)) + + Button( + onClick = onSeeAllButtonClick, + colors = ButtonDefaults.outlinedButtonColors( + backgroundColor = if (tab == null) { + FirefoxTheme.colors.layer3 + } else { + FirefoxTheme.colors.actionSecondary + } + ), + elevation = ButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp + ), + modifier = Modifier + .height(36.dp) + .fillMaxWidth() + ) { + if (tab != null) { + Text( + text = stringResource(R.string.recent_tabs_see_all_synced_tabs_button_text), + textAlign = TextAlign.Center, + color = FirefoxTheme.colors.textActionSecondary + ) + } + } + } + } +} + /** * A recent tab image. * - * @param tab [RecentTab.Tab] that was recently viewed. + * @param tab [RecentTab] that was recently viewed. * @param modifier [Modifier] used to draw the image content. * @param contentScale [ContentScale] used to draw image content. * @param alignment [Alignment] used to draw the image content. - * is null. */ @Composable @Suppress("LongParameterList") private fun RecentTabImage( - tab: RecentTab.Tab, + tab: RecentTab, modifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.FillWidth, alignment: Alignment = Alignment.TopCenter ) { - val previewImageUrl = tab.state.content.previewImageUrl - val thumbnail = tab.state.content.thumbnail + val (previewImageUrl, loaderUrl, key) = when (tab) { + is RecentTab.Tab -> Triple( + tab.state.content.previewImageUrl, + tab.state.content.url, + tab.state.id + ) + is RecentTab.SyncedTab -> Triple(tab.previewImageUrl, tab.url, tab.url) + else -> return + } + val thumbnail = (tab as? RecentTab.Tab)?.state?.content?.thumbnail when { !previewImageUrl.isNullOrEmpty() -> { @@ -287,15 +418,10 @@ private fun RecentTabImage( modifier = modifier, backgroundColor = colorResource(id = R.color.photonGrey20) ) { - components.core.icons.Loader(tab.state.content.url) { + components.core.icons.Loader(loaderUrl) { Placeholder { Box( - modifier = Modifier.background( - color = when (isSystemInDarkTheme()) { - true -> PhotonColors.DarkGrey30 - false -> PhotonColors.LightGrey30 - } - ) + modifier = Modifier.background(color = FirefoxTheme.colors.layer3) ) } @@ -317,7 +443,7 @@ private fun RecentTabImage( } ThumbnailImage( - tabId = tab.state.id, + key = key, modifier = modifier, contentScale = contentScale, alignment = alignment @@ -327,6 +453,19 @@ private fun RecentTabImage( } } +/** + * A placeholder for a recent tab image. + */ +@Composable +private fun RecentTabImagePlaceholder() { + Box( + modifier = Modifier + .size(108.dp, 80.dp) + .clip(RoundedCornerShape(8.dp)) + .background(color = FirefoxTheme.colors.layer3) + ) +} + /** * Menu shown for a [RecentTab.Tab]. * @@ -444,6 +583,20 @@ private fun RecentTabTitle(title: String) { ) } +/** + * A placeholder for a tab title. + */ +@Composable +private fun RecentTabTitlePlaceholder() { + Column { + TextLinePlaceHolder() + + Spacer(modifier = Modifier.height(8.dp)) + + TextLinePlaceHolder() + } +} + /** * A recent tab subtitle. * @@ -465,18 +618,18 @@ private fun RecentTabSubtitle(subtitle: String) { @Composable private fun ThumbnailImage( - tabId: String, + key: String, modifier: Modifier, contentScale: ContentScale, alignment: Alignment ) { - val rememberBitmap = remember(tabId) { mutableStateOf(null) } + val rememberBitmap = remember(key) { mutableStateOf(null) } val size = LocalDensity.current.run { 108.dp.toPx().toInt() } - val request = ImageLoadRequest(tabId, size) + val request = ImageLoadRequest(key, size) val storage = components.core.thumbnailStorage val bitmap = rememberBitmap.value - LaunchedEffect(tabId) { + LaunchedEffect(key) { rememberBitmap.value = storage.loadThumbnail(request).await()?.asImageBitmap() } @@ -491,3 +644,13 @@ private fun ThumbnailImage( ) } } + +@Composable +private fun TextLinePlaceHolder() { + Box( + modifier = Modifier + .height(12.dp) + .fillMaxWidth() + .background(FirefoxTheme.colors.layer3) + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0ae43ba52..57470ac4d8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -127,6 +127,10 @@ %d sites + + See all synced tabs + + Synced device Remove