For #19942 - Add support for sticky headers to the synced tabs list

upstream-sync
Noah Bond 2 years ago committed by mergify[bot]
parent 977d2f2b3a
commit bd4742004c

@ -110,4 +110,9 @@ object FeatureFlags {
listOf("en-US", "es-US").contains(langTag) && Config.channel.isNightlyOrDebug
}
}
/**
* Enables the Task Continuity enhancements.
*/
val taskContinuityFeature = Config.channel.isDebug
}

@ -0,0 +1,41 @@
/* 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.tabstray.ext
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem
/**
* Converts a list of [SyncedDeviceTabs] into a list of [SyncedTabsListItem].
*/
fun List<SyncedDeviceTabs>.toComposeList(): List<SyncedTabsListItem> = asSequence().flatMap { (device, tabs) ->
if (FeatureFlags.taskContinuityFeature) {
val deviceTabs = if (tabs.isEmpty()) {
emptyList()
} else {
tabs.map {
val url = it.active().url
val titleText = it.active().title.ifEmpty { url.take(MAX_URI_LENGTH) }
SyncedTabsListItem.Tab(titleText, url, it)
}
}
sequenceOf(SyncedTabsListItem.DeviceSection(device.displayName, deviceTabs))
} else {
val deviceTabs = if (tabs.isEmpty()) {
sequenceOf(SyncedTabsListItem.NoTabs)
} else {
tabs.asSequence().map {
val url = it.active().url
val titleText = it.active().title.ifEmpty { url.take(MAX_URI_LENGTH) }
SyncedTabsListItem.Tab(titleText, url, it)
}
}
sequenceOf(SyncedTabsListItem.Device(device.displayName)) + deviceTabs
}
}.toList()

@ -9,6 +9,7 @@ package org.mozilla.fenix.tabstray.syncedtabs
import android.content.res.Configuration
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@ -45,10 +46,9 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.browser.storage.sync.TabEntry
import mozilla.components.browser.toolbar.MAX_URI_LENGTH
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.PrimaryText
import org.mozilla.fenix.compose.SecondaryText
@ -61,6 +61,7 @@ import mozilla.components.browser.storage.sync.Tab as SyncTab
* @param syncedTabs The tab UI items to be displayed.
* @param onTabClick The lambda for handling clicks on synced tabs.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SyncedTabsList(syncedTabs: List<SyncedTabsListItem>, onTabClick: (SyncTab) -> Unit) {
val listState = rememberLazyListState()
@ -69,20 +70,54 @@ fun SyncedTabsList(syncedTabs: List<SyncedTabsListItem>, onTabClick: (SyncTab) -
modifier = Modifier.fillMaxSize(),
state = listState,
) {
items(syncedTabs) { syncedTabItem ->
when (syncedTabItem) {
is SyncedTabsListItem.Device -> SyncedTabsDeviceItem(deviceName = syncedTabItem.displayName)
is SyncedTabsListItem.Error -> SyncedTabsErrorItem(
errorText = syncedTabItem.errorText,
errorButton = syncedTabItem.errorButton
)
is SyncedTabsListItem.NoTabs -> SyncedTabsNoTabsItem()
is SyncedTabsListItem.Tab -> {
SyncedTabsTabItem(
tabTitleText = syncedTabItem.displayTitle,
url = syncedTabItem.displayURL,
) {
onTabClick(syncedTabItem.tab)
if (FeatureFlags.taskContinuityFeature) {
syncedTabs.forEach { syncedTabItem ->
when (syncedTabItem) {
is SyncedTabsListItem.DeviceSection -> {
stickyHeader {
SyncedTabsDeviceItem(deviceName = syncedTabItem.displayName)
}
if (syncedTabItem.tabs.isNotEmpty()) {
items(syncedTabItem.tabs) { syncedTab ->
SyncedTabsTabItem(
tabTitleText = syncedTab.displayTitle,
url = syncedTab.displayURL,
) {
onTabClick(syncedTab.tab)
}
}
} else {
item { SyncedTabsNoTabsItem() }
}
}
is SyncedTabsListItem.Error -> {
item {
SyncedTabsErrorItem(
errorText = syncedTabItem.errorText,
errorButton = syncedTabItem.errorButton
)
}
}
}
}
} else {
items(syncedTabs) { syncedTabItem ->
when (syncedTabItem) {
is SyncedTabsListItem.Device -> SyncedTabsDeviceItem(deviceName = syncedTabItem.displayName)
is SyncedTabsListItem.Error -> SyncedTabsErrorItem(
errorText = syncedTabItem.errorText,
errorButton = syncedTabItem.errorButton
)
is SyncedTabsListItem.NoTabs -> SyncedTabsNoTabsItem()
is SyncedTabsListItem.Tab -> {
SyncedTabsTabItem(
tabTitleText = syncedTabItem.displayTitle,
url = syncedTabItem.displayURL,
) {
onTabClick(syncedTabItem.tab)
}
}
}
}
@ -103,7 +138,11 @@ fun SyncedTabsList(syncedTabs: List<SyncedTabsListItem>, onTabClick: (SyncTab) -
*/
@Composable
fun SyncedTabsDeviceItem(deviceName: String) {
Column(Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(FirefoxTheme.colors.layer1)
) {
PrimaryText(
text = deviceName,
modifier = Modifier
@ -307,35 +346,19 @@ private fun SyncedTabsListPreview() {
}
}
/**
* Converts a list of [SyncedDeviceTabs] into a list of [SyncedTabsListItem].
*/
fun List<SyncedDeviceTabs>.toComposeList() = asSequence().flatMap { (device, tabs) ->
// Transform to sticky headers data here https://github.com/mozilla-mobile/fenix/issues/19942
val deviceTabs = if (tabs.isEmpty()) {
sequenceOf(SyncedTabsListItem.NoTabs)
} else {
tabs.asSequence().map {
val url = it.active().url
val titleText = it.active().title.ifEmpty { url.take(MAX_URI_LENGTH) }
SyncedTabsListItem.Tab(titleText, url, it)
}
}
sequenceOf(SyncedTabsListItem.Device(device.displayName)) + deviceTabs
}.toList()
/**
* Helper function to create a List of [SyncedTabsListItem] for previewing.
*/
@VisibleForTesting internal fun getFakeSyncedTabList(): List<SyncedTabsListItem> = listOf(
SyncedTabsListItem.Device("Device 1"),
generateFakeTab("Mozilla", "www.mozilla.org"),
generateFakeTab("Google", "www.google.com"),
generateFakeTab("", "www.google.com"),
SyncedTabsListItem.Device("Device 2"),
SyncedTabsListItem.NoTabs,
SyncedTabsListItem.Device("Device 3"),
SyncedTabsListItem.DeviceSection(
displayName = "Device 1",
tabs = listOf(
generateFakeTab("Mozilla", "www.mozilla.org"),
generateFakeTab("Google", "www.google.com"),
generateFakeTab("", "www.google.com"),
)
),
SyncedTabsListItem.DeviceSection("Device 2", emptyList()),
SyncedTabsListItem.Error("Please re-authenticate"),
)

@ -20,6 +20,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.FloatingActionButtonBinding
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.ext.toComposeList
/**
* TabsTrayFragment delegate to handle all layout updates needed to display synced tabs and any errors.

@ -18,6 +18,14 @@ sealed class SyncedTabsListItem {
*/
data class Device(val displayName: String) : SyncedTabsListItem()
/**
* A section for displaying a synced device and its tabs.
*
* @param displayName The user's custom name of their synced device.
* @param tabs The user's tabs from their synced device.
*/
data class DeviceSection(val displayName: String, val tabs: List<Tab>) : SyncedTabsListItem()
/**
* A tab that was synced.
*

@ -13,10 +13,10 @@ import mozilla.components.concept.sync.DeviceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.tabstray.ext.toComposeList
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem
import org.mozilla.fenix.tabstray.syncedtabs.toComposeList
class SyncedTabsListItemTest {
class SyncedDeviceTabsTest {
private val noTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
@ -80,25 +80,24 @@ class SyncedTabsListItemTest {
)
@Test
fun `verify ordering of list items`() {
fun `GIVEN two synced devices WHEN the compose list is generated THEN two device section is returned`() {
val syncedDeviceList = listOf(oneTabDevice, twoTabDevice)
val listData = syncedDeviceList.toComposeList()
assertEquals(5, listData.count())
assertTrue(listData[0] is SyncedTabsListItem.Device)
assertTrue(listData[1] is SyncedTabsListItem.Tab)
assertTrue(listData[2] is SyncedTabsListItem.Device)
assertTrue(listData[3] is SyncedTabsListItem.Tab)
assertTrue(listData[4] is SyncedTabsListItem.Tab)
assertEquals(2, listData.count())
assertTrue(listData[0] is SyncedTabsListItem.DeviceSection)
assertEquals(oneTabDevice.tabs.size, (listData[0] as SyncedTabsListItem.DeviceSection).tabs.size)
assertTrue(listData[1] is SyncedTabsListItem.DeviceSection)
assertEquals(twoTabDevice.tabs.size, (listData[1] as SyncedTabsListItem.DeviceSection).tabs.size)
}
@Test
fun `verify no tabs displayed`() {
fun `GIVEN one synced device with no tabs WHEN the compose list is generated THEN one device with an empty tabs list is returned`() {
val syncedDeviceList = listOf(noTabDevice)
val adapterData = syncedDeviceList.toComposeList()
val listData = syncedDeviceList.toComposeList()
assertEquals(2, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsListItem.Device)
assertTrue(adapterData[1] is SyncedTabsListItem.NoTabs)
assertEquals(1, listData.count())
assertTrue(listData[0] is SyncedTabsListItem.DeviceSection)
assertEquals(0, (listData[0] as SyncedTabsListItem.DeviceSection).tabs.size)
}
}
Loading…
Cancel
Save