[fenix] For https://github.com/mozilla-mobile/fenix/issues/21900 - Delete files from Synced Tabs XML implementation

pull/600/head
Noah Bond 2 years ago committed by mergify[bot]
parent 71f539bf4e
commit 25817127da

@ -70,6 +70,7 @@ class FloatingActionButtonBinding(
false -> R.string.tab_drawer_fab_sync
}
)
contentDescription = context.getString(R.string.resync_button_content_description)
extend()
show()
setIconResource(R.drawable.ic_fab_sync)

@ -61,11 +61,13 @@ import org.mozilla.fenix.tabstray.ext.make
import org.mozilla.fenix.tabstray.ext.orDefault
import org.mozilla.fenix.tabstray.ext.showWithTheme
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsIntegration
import org.mozilla.fenix.utils.allowUndo
import kotlin.math.max
@Suppress("TooManyFunctions", "LargeClass")
class TabsTrayFragment : AppCompatDialogFragment() {
@VisibleForTesting internal lateinit var tabsTrayStore: TabsTrayStore
private lateinit var browserTrayInteractor: BrowserTrayInteractor
private lateinit var tabsTrayInteractor: TabsTrayInteractor
@ -82,6 +84,7 @@ class TabsTrayFragment : AppCompatDialogFragment() {
private val secureTabsTrayBinding = ViewBoundFeatureWrapper<SecureTabsTrayBinding>()
private val tabsFeature = ViewBoundFeatureWrapper<TabsFeature>()
private val tabsTrayInactiveTabsOnboardingBinding = ViewBoundFeatureWrapper<TabsTrayInactiveTabsOnboardingBinding>()
private val syncedTabsIntegration = ViewBoundFeatureWrapper<SyncedTabsIntegration>()
@VisibleForTesting @Suppress("VariableNaming")
internal var _tabsTrayBinding: ComponentTabstray2Binding? = null
@ -370,6 +373,19 @@ class TabsTrayFragment : AppCompatDialogFragment() {
view = view
)
syncedTabsIntegration.set(
feature = SyncedTabsIntegration(
store = tabsTrayStore,
context = requireContext(),
navController = findNavController(),
storage = requireComponents.backgroundServices.syncedTabsStorage,
accountManager = requireComponents.backgroundServices.accountManager,
lifecycleOwner = this
),
owner = this,
view = view
)
setFragmentResultListener(ShareFragment.RESULT_KEY) { _, _ ->
dismissTabsTray()
}
@ -461,7 +477,7 @@ class TabsTrayFragment : AppCompatDialogFragment() {
navigationInteractor,
trayInteractor,
requireComponents.core.store,
requireComponents.appStore
requireComponents.appStore,
)
isUserInputEnabled = false
}

@ -11,6 +11,7 @@ import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem
/**
* Value type that represents the state of the tabs tray.
@ -32,6 +33,7 @@ data class TabsTrayState(
val searchTermPartition: TabPartition? = null,
val normalTabs: List<TabSessionState> = emptyList(),
val privateTabs: List<TabSessionState> = emptyList(),
val syncedTabs: List<SyncedTabsListItem> = emptyList(),
val syncing: Boolean = false,
val focusGroupTabId: String? = null
) : State {
@ -155,6 +157,11 @@ sealed class TabsTrayAction : Action {
* Updates the list of tabs in [TabsTrayState.privateTabs].
*/
data class UpdatePrivateTabs(val tabs: List<TabSessionState>) : TabsTrayAction()
/**
* Updates the list of synced tabs in [TabsTrayState.syncedTabs].
*/
data class UpdateSyncedTabs(val tabs: List<SyncedTabsListItem>) : TabsTrayAction()
}
/**
@ -195,6 +202,8 @@ internal object TabsTrayReducer {
state.copy(normalTabs = action.tabs)
is TabsTrayAction.UpdatePrivateTabs ->
state.copy(privateTabs = action.tabs)
is TabsTrayAction.UpdateSyncedTabs ->
state.copy(syncedTabs = action.tabs)
}
}
}

@ -8,18 +8,17 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.TitleHeaderAdapter
import org.mozilla.fenix.tabstray.browser.InactiveTabsAdapter
import org.mozilla.fenix.tabstray.browser.TabGroupAdapter
import org.mozilla.fenix.tabstray.syncedtabs.TabClickDelegate
import org.mozilla.fenix.tabstray.browser.TitleHeaderAdapter
import org.mozilla.fenix.tabstray.viewholders.AbstractPageViewHolder
import org.mozilla.fenix.tabstray.viewholders.NormalBrowserPageViewHolder
import org.mozilla.fenix.tabstray.viewholders.PrivateBrowserPageViewHolder
@ -57,17 +56,12 @@ class TrayPagerAdapter(
TABS_TRAY_FEATURE_NAME
)
}
private val syncedTabsAdapter by lazy {
SyncedTabsAdapter(TabClickDelegate(navInteractor))
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractPageViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractPageViewHolder =
when (viewType) {
NormalBrowserPageViewHolder.LAYOUT_ID -> {
NormalBrowserPageViewHolder(
itemView,
LayoutInflater.from(parent.context).inflate(viewType, parent, false),
tabsTrayStore,
browserStore,
appStore,
@ -76,7 +70,7 @@ class TrayPagerAdapter(
}
PrivateBrowserPageViewHolder.LAYOUT_ID -> {
PrivateBrowserPageViewHolder(
itemView,
LayoutInflater.from(parent.context).inflate(viewType, parent, false),
tabsTrayStore,
browserStore,
interactor
@ -84,22 +78,30 @@ class TrayPagerAdapter(
}
SyncedTabsPageViewHolder.LAYOUT_ID -> {
SyncedTabsPageViewHolder(
itemView,
tabsTrayStore
composeView = ComposeView(parent.context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
},
tabsTrayStore = tabsTrayStore,
navigationInteractor = navInteractor
)
}
else -> throw IllegalStateException("Unknown viewType.")
}
}
/**
* Until [TrayPagerAdapter] is replaced with a Compose implementation, [SyncedTabsPageViewHolder]
* will need to be called with an empty bind() function since it no longer needs an adapter to render.
* For more details: https://github.com/mozilla-mobile/fenix/issues/21318
*/
override fun onBindViewHolder(viewHolder: AbstractPageViewHolder, position: Int) {
val adapter = when (position) {
POSITION_NORMAL_TABS -> normalAdapter
POSITION_PRIVATE_TABS -> privateAdapter
POSITION_SYNCED_TABS -> syncedTabsAdapter
else -> throw IllegalStateException("View type does not exist.")
when (viewHolder) {
is NormalBrowserPageViewHolder -> viewHolder.bind(normalAdapter)
is PrivateBrowserPageViewHolder -> viewHolder.bind(privateAdapter)
is SyncedTabsPageViewHolder -> viewHolder.bind()
}
viewHolder.bind(adapter)
}
override fun getItemViewType(position: Int): Int {

@ -0,0 +1,353 @@
/* 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/. */
@file:Suppress("TooManyFunctions")
package org.mozilla.fenix.tabstray.syncedtabs
import android.content.res.Configuration
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
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.IntrinsicSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
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.R
import org.mozilla.fenix.compose.PrimaryText
import org.mozilla.fenix.compose.SecondaryText
import org.mozilla.fenix.theme.FirefoxTheme
import mozilla.components.browser.storage.sync.Tab as SyncTab
/**
* Top-level list UI for displaying Synced Tabs in the Tabs Tray.
*
* @param syncedTabs The tab UI items to be displayed.
* @param onTabClick The lambda for handling clicks on synced tabs.
*/
@Composable
fun SyncedTabsList(syncedTabs: List<SyncedTabsListItem>, onTabClick: (SyncTab) -> Unit) {
val listState = rememberLazyListState()
LazyColumn(
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)
}
}
}
}
item {
// The Spacer here is to act as a footer to add padding to the bottom of the list so
// the FAB or any potential SnackBar doesn't overlap with the items at the end.
Spacer(Modifier.height(240.dp))
}
}
}
/**
* Text header for sections of synced tabs
*
* @param deviceName The name of the user's device connected that has synced tabs.
*/
@Composable
fun SyncedTabsDeviceItem(deviceName: String) {
Column(Modifier.fillMaxWidth()) {
PrimaryText(
text = deviceName,
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, top = 16.dp, end = 8.dp, bottom = 8.dp),
fontSize = 14.sp,
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
maxLines = 1
)
Divider(color = FirefoxTheme.colors.borderPrimary)
}
}
/**
* Synced tab list item UI
*
* @param tabTitleText The tab's display text.
* @param url The tab's URL.
* @param onClick The click handler when this synced tab is clicked.
*/
@Composable
fun SyncedTabsTabItem(tabTitleText: String, url: String, onClick: () -> Unit) {
Column(
modifier = Modifier
.clickable(
onClickLabel = tabTitleText,
onClick = onClick
)
.padding(horizontal = 16.dp)
.defaultMinSize(minHeight = 56.dp),
verticalArrangement = Arrangement.Center,
) {
PrimaryText(
text = tabTitleText,
modifier = Modifier.fillMaxWidth(),
fontSize = 16.sp,
maxLines = 1
)
SecondaryText(
text = url,
modifier = Modifier
.fillMaxWidth()
.padding(top = 2.dp),
fontSize = 12.sp,
maxLines = 1
)
}
}
/**
* Error UI to show if there is one of the errors outlined in [SyncedTabsView.ErrorType].
*
* @param errorText The text to be displayed to the user.
* @param errorButton Optional class to set up and handle any clicks in the Error UI.
*/
@Composable
fun SyncedTabsErrorItem(errorText: String, errorButton: SyncedTabsListItem.ErrorButton? = null) {
Box(
Modifier
.padding(all = 16.dp)
.height(IntrinsicSize.Min)
) {
val dashColor = FirefoxTheme.colors.borderPrimary
Canvas(Modifier.fillMaxSize()) {
drawRoundRect(
color = dashColor,
style = Stroke(
width = 2.dp.toPx(),
pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 4.dp.toPx()), 0f)
),
cornerRadius = CornerRadius(
x = 8.dp.toPx(),
y = 8.dp.toPx()
),
)
}
Column(
Modifier
.padding(all = 16.dp)
.fillMaxWidth()
) {
PrimaryText(
text = errorText,
modifier = Modifier.fillMaxWidth(),
fontSize = 14.sp
)
errorButton?.let {
Spacer(modifier = Modifier.height(12.dp))
SyncedTabsErrorButton(buttonText = it.buttonText, onClick = it.onClick)
}
}
}
}
/**
* Error button UI within SyncedTabsErrorItem
*
* @param buttonText The error button's text and accessibility hint.
* @param onClick The lambda called when the button is clicked.
*/
@Composable
fun SyncedTabsErrorButton(buttonText: String, onClick: () -> Unit) {
Button(
onClick = onClick,
modifier = Modifier.clip(RoundedCornerShape(size = 4.dp)),
elevation = ButtonDefaults.elevation(defaultElevation = 0.dp, pressedElevation = 0.dp),
colors = ButtonDefaults.outlinedButtonColors(backgroundColor = FirefoxTheme.colors.actionPrimary),
) {
Icon(
painter = painterResource(R.drawable.ic_sign_in),
contentDescription = null,
tint = FirefoxTheme.colors.textOnColor,
)
Spacer(Modifier.width(8.dp))
Text(
text = buttonText,
modifier = Modifier.align(Alignment.CenterVertically),
color = FirefoxTheme.colors.textOnColor,
fontSize = 14.sp,
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
maxLines = 2
)
}
}
/**
* UI to be displayed when a user's device has no synced tabs.
*/
@Composable
fun SyncedTabsNoTabsItem() {
SecondaryText(
text = stringResource(R.string.synced_tabs_no_open_tabs),
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth(),
fontSize = 16.sp,
maxLines = 1
)
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SyncedTabsListItemsPreview() {
FirefoxTheme {
Column(Modifier.background(FirefoxTheme.colors.layer1)) {
SyncedTabsDeviceItem(deviceName = "Google Pixel Pro Max +Ultra 5000")
Spacer(Modifier.height(16.dp))
SyncedTabsTabItem(tabTitleText = "Mozilla", url = "www.mozilla.org") { println("Clicked tab") }
Spacer(Modifier.height(16.dp))
SyncedTabsErrorItem(errorText = stringResource(R.string.synced_tabs_reauth))
Spacer(Modifier.height(16.dp))
SyncedTabsNoTabsItem()
Spacer(Modifier.height(16.dp))
}
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SyncedTabsErrorPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
SyncedTabsErrorItem(
errorText = stringResource(R.string.synced_tabs_no_tabs),
errorButton = SyncedTabsListItem.ErrorButton(
buttonText = stringResource(R.string.synced_tabs_sign_in_button)
) {
println("SyncedTabsErrorButton click")
},
)
}
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SyncedTabsListPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
SyncedTabsList(
getFakeSyncedTabList(),
) {
println("Tab clicked")
}
}
}
}
/**
* 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.Error("Please re-authenticate"),
)
/**
* Helper function to create a [SyncedTabsListItem.Tab] for previewing.
*/
private fun generateFakeTab(tabName: String, tabUrl: String): SyncedTabsListItem.Tab = SyncedTabsListItem.Tab(
tabName.ifEmpty { tabUrl },
tabUrl,
SyncTab(
history = listOf(TabEntry(tabName, tabUrl, null)),
active = 0,
lastUsed = 0L,
)
)

@ -0,0 +1,122 @@
/* 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.syncedtabs
import android.content.Context
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import mozilla.components.browser.storage.sync.SyncedDeviceTabs
import mozilla.components.feature.syncedtabs.SyncedTabsFeature
import mozilla.components.feature.syncedtabs.storage.SyncedTabsStorage
import mozilla.components.feature.syncedtabs.view.SyncedTabsView
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.tabstray.FloatingActionButtonBinding
import org.mozilla.fenix.tabstray.TabsTrayAction
import org.mozilla.fenix.tabstray.TabsTrayStore
/**
* TabsTrayFragment delegate to handle all layout updates needed to display synced tabs and any errors.
*
* @param store [TabsTrayStore]
* @param context Fragment context.
* @param navController The controller used to handle any navigation necessary for error scenarios.
* @param storage An instance of [SyncedTabsStorage] used for retrieving synced tabs.
* @param accountManager An instance of [FxaAccountManager] used for synced tabs authentication.
* @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
*/
class SyncedTabsIntegration(
private val store: TabsTrayStore,
private val context: Context,
private val navController: NavController,
storage: SyncedTabsStorage,
accountManager: FxaAccountManager,
lifecycleOwner: LifecycleOwner,
) : LifecycleAwareFeature,
SyncedTabsView,
Observable<SyncedTabsView.Listener> by ObserverRegistry() {
private val syncedTabsFeature by lazy {
SyncedTabsFeature(
context = context,
storage = storage,
accountManager = accountManager,
view = this,
lifecycleOwner = lifecycleOwner,
onTabClicked = {
// We can ignore this callback here because we're not connecting the Compose UI
// back to the feature.
}
)
}
private val syncButtonBinding by lazy {
SyncButtonBinding(store) { listener?.onRefresh() }
}
override var listener: SyncedTabsView.Listener? = null
override fun start() {
syncedTabsFeature.start()
syncButtonBinding.start()
}
override fun stop() {
syncedTabsFeature.stop()
syncButtonBinding.stop()
}
override fun onError(error: SyncedTabsView.ErrorType) {
// We may still be displaying a "loading" spinner, hide it.
stopLoading()
store.dispatch(TabsTrayAction.UpdateSyncedTabs(listOf(error.toSyncedTabsListItem())))
}
/**
* Do nothing; the UI is handled with [FloatingActionButtonBinding].
*/
override fun startLoading() = Unit
override fun stopLoading() {
store.dispatch(TabsTrayAction.SyncCompleted)
}
override fun displaySyncedTabs(syncedTabs: List<SyncedDeviceTabs>) {
store.dispatch(TabsTrayAction.UpdateSyncedTabs(syncedTabs.toComposeList()))
}
/**
* Converts [SyncedTabsView.ErrorType] to [SyncedTabsListItem.Error] with a lambda for ONLY
* [SyncedTabsView.ErrorType.SYNC_UNAVAILABLE]
*/
private fun SyncedTabsView.ErrorType.toSyncedTabsListItem() = when (this) {
SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE ->
SyncedTabsListItem.Error(errorText = context.getString(R.string.synced_tabs_connect_another_device))
SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE ->
SyncedTabsListItem.Error(errorText = context.getString(R.string.synced_tabs_enable_tab_syncing))
SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION ->
SyncedTabsListItem.Error(errorText = context.getString(R.string.synced_tabs_sign_in_message))
SyncedTabsView.ErrorType.NO_TABS_AVAILABLE ->
SyncedTabsListItem.Error(errorText = context.getString(R.string.synced_tabs_reauth))
SyncedTabsView.ErrorType.SYNC_UNAVAILABLE ->
SyncedTabsListItem.Error(
errorText = context.getString(R.string.synced_tabs_no_tabs),
errorButton = SyncedTabsListItem.ErrorButton(
buttonText = context.getString(R.string.synced_tabs_sign_in_button)
) {
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
},
)
}
}

@ -0,0 +1,61 @@
/* 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.syncedtabs
import mozilla.components.browser.storage.sync.Tab as SyncTab
/**
* The various types of list items that can be found in a [SyncedTabsList].
*/
sealed class SyncedTabsListItem {
/**
* A device header for displaying a synced device.
*
* @param displayName The user's custom name of their synced device.
*/
data class Device(val displayName: String) : SyncedTabsListItem()
/**
* A tab that was synced.
*
* @param displayTitle The title of the tab's web page.
* @param displayURL The tab's URL up to BrowserToolbar.MAX_URI_LENGTH characters long.
* @param tab The underlying SyncTab object passed when the tab is clicked.
*/
data class Tab(
val displayTitle: String,
val displayURL: String,
val tab: SyncTab
) : SyncedTabsListItem()
/**
* A placeholder for a device that has no tabs synced.
*/
object NoTabs : SyncedTabsListItem()
/**
* A message displayed if an error was encountered.
*
* @param errorText The text to be displayed to the user.
* @param errorButton Optional class to set up and handle any clicks in the Error UI.
*/
data class Error(
val errorText: String,
val errorButton: ErrorButton? = null,
) : SyncedTabsListItem()
/**
* A button displayed if an error has optional interaction.
*
* @param buttonText The error button's text and accessibility hint.
* @param onClick Lambda called when the button is clicked.
*
*/
data class ErrorButton(
val buttonText: String,
val onClick: () -> Unit
)
}

@ -5,32 +5,47 @@
package org.mozilla.fenix.tabstray.viewholders
import android.view.View
import androidx.recyclerview.widget.GridLayoutManager
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.ComponentSyncTabsTrayLayoutBinding
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.tabstray.NavigationInteractor
import org.mozilla.fenix.tabstray.TabsTrayStore
import org.mozilla.fenix.tabstray.TabsTrayState
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsList
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Temporary ViewHolder to render [SyncedTabsList] until all of the Tabs Tray is written in Compose.
*
* @param composeView Root ComposeView passed-in from TrayPagerAdapter.
* @param tabsTrayStore Store used as a Composable State to listen for changes to [TabsTrayState.syncedTabs].
* @param navigationInteractor The lambda for handling clicks on synced tabs.
*/
class SyncedTabsPageViewHolder(
containerView: View,
private val tabsTrayStore: TabsTrayStore
) : AbstractPageViewHolder(containerView) {
override fun bind(
adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>
) {
val binding = ComponentSyncTabsTrayLayoutBinding.bind(containerView)
binding.syncedTabsList.layoutManager = GridLayoutManager(containerView.context, 1)
binding.syncedTabsList.adapter = adapter
binding.syncedTabsTrayLayout.tabsTrayStore = tabsTrayStore
private val composeView: ComposeView,
private val tabsTrayStore: TabsTrayStore,
private val navigationInteractor: NavigationInteractor,
) : AbstractPageViewHolder(composeView) {
fun bind() {
composeView.setContent {
val tabs = tabsTrayStore.observeAsComposableState { state -> state.syncedTabs }.value
FirefoxTheme {
SyncedTabsList(
syncedTabs = tabs ?: emptyList(),
onTabClick = navigationInteractor::onSyncedTabClicked
)
}
}
}
override fun bind(adapter: RecyclerView.Adapter<out RecyclerView.ViewHolder>) = Unit // no-op
override fun detachedFromWindow() = Unit // no-op
override fun attachedToWindow() = Unit // no-op
companion object {
const val LAYOUT_ID = R.layout.component_sync_tabs_tray_layout
val LAYOUT_ID = View.generateViewId()
}
}

@ -176,9 +176,6 @@
<dimen name="saved_logins_item_margin_start">16dp</dimen>
<dimen name="saved_logins_item_margin_end">48dp</dimen>
<!-- Synced Tabs Fragment -->
<dimen name="synced_tabs_error_margin">20dp</dimen>
<!-- Account Settings Fragment -->
<dimen name="account_settings_device_name_min_height">48dp</dimen>

@ -165,7 +165,7 @@
<!-- Browser menu toggle that installs a Progressive Web App shortcut to the site on the device home screen. -->
<string name="browser_menu_install_on_homescreen">Install</string>
<!-- Menu option on the toolbar that takes you to synced tabs page-->
<string name="synced_tabs">Synced tabs</string>
<string name="synced_tabs" moz:removedIn="98" tools:ignore="UnusedResources">Synced tabs</string>
<!-- Content description (not visible, for screen readers etc.) for the Resync tabs button -->
<string name="resync_button_content_description">Resync</string>
<!-- Browser menu button that opens the find in page menu -->

@ -13,12 +13,14 @@ import mozilla.components.concept.sync.DeviceType
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mozilla.fenix.sync.SyncedTabsAdapter
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsListItem
import org.mozilla.fenix.tabstray.syncedtabs.toComposeList
class SyncedTabsAdapterKtTest {
class SyncedTabsListItemTest {
private val noTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { id } returns "123"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = emptyList()
@ -27,6 +29,7 @@ class SyncedTabsAdapterKtTest {
private val oneTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Charcoal"
every { id } returns "1234"
every { deviceType } returns DeviceType.DESKTOP
},
tabs = listOf(
@ -47,6 +50,7 @@ class SyncedTabsAdapterKtTest {
private val twoTabDevice = SyncedDeviceTabs(
device = mockk {
every { displayName } returns "Emerald"
every { id } returns "12345"
every { deviceType } returns DeviceType.MOBILE
},
tabs = listOf(
@ -76,25 +80,25 @@ class SyncedTabsAdapterKtTest {
)
@Test
fun `verify ordering of adapter items`() {
fun `verify ordering of list items`() {
val syncedDeviceList = listOf(oneTabDevice, twoTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
val listData = syncedDeviceList.toComposeList()
assertEquals(5, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[2] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[3] is SyncedTabsAdapter.AdapterItem.Tab)
assertTrue(adapterData[4] is SyncedTabsAdapter.AdapterItem.Tab)
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)
}
@Test
fun `verify no tabs displayed`() {
val syncedDeviceList = listOf(noTabDevice)
val adapterData = syncedDeviceList.toAdapterList()
val adapterData = syncedDeviceList.toComposeList()
assertEquals(2, adapterData.count())
assertTrue(adapterData[0] is SyncedTabsAdapter.AdapterItem.Device)
assertTrue(adapterData[1] is SyncedTabsAdapter.AdapterItem.NoTabs)
assertTrue(adapterData[0] is SyncedTabsListItem.Device)
assertTrue(adapterData[1] is SyncedTabsListItem.NoTabs)
}
}

@ -122,7 +122,7 @@ class FloatingActionButtonBindingTest {
verify(exactly = 0) { actionButton.hide() }
verify(exactly = 1) { actionButton.setText(R.string.tab_drawer_fab_sync) }
verify(exactly = 1) { actionButton.setIconResource(R.drawable.ic_fab_sync) }
verify(exactly = 2) { actionButton.contentDescription = any() }
verify(exactly = 3) { actionButton.contentDescription = any() }
tabsTrayStore.dispatch(TabsTrayAction.SyncNow)
tabsTrayStore.waitUntilIdle()
@ -133,6 +133,6 @@ class FloatingActionButtonBindingTest {
verify(exactly = 0) { actionButton.hide() }
verify(exactly = 1) { actionButton.setText(R.string.sync_syncing_in_progress) }
verify(exactly = 2) { actionButton.setIconResource(R.drawable.ic_fab_sync) }
verify(exactly = 2) { actionButton.contentDescription = any() }
verify(exactly = 4) { actionButton.contentDescription = any() }
}
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.tabstray
import mozilla.components.browser.state.state.createTab
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.tabstray.syncedtabs.getFakeSyncedTabList
class TabsTrayStoreReducerTest {
@Test
@ -69,4 +70,18 @@ class TabsTrayStoreReducerTest {
assertEquals(expectedState, resultState)
}
@Test
fun `WHEN UpdateSyncedTabs THEN synced tabs are added`() {
val syncedTabs = getFakeSyncedTabList()
val initialState = TabsTrayState()
val expectedState = initialState.copy(syncedTabs = syncedTabs)
val resultState = TabsTrayReducer.reduce(
initialState,
TabsTrayAction.UpdateSyncedTabs(syncedTabs)
)
assertEquals(expectedState, resultState)
}
}

Loading…
Cancel
Save