Closes #18946: Add undo toast for tabstray

upstream-sync
Roger Yang 3 years ago committed by Jonathan Almeida
parent cbc5df3c63
commit cba68faac6

@ -23,11 +23,11 @@ class TabLayoutMediator(
private val tabLayout: TabLayout,
interactor: TabsTrayInteractor,
private val browserStore: BrowserStore,
trayStore: TabsTrayStore,
private val tabsTrayStore: TabsTrayStore,
private val metrics: MetricController
) : LifecycleAwareFeature {
private val observer = TabLayoutObserver(interactor, trayStore, metrics)
private val observer = TabLayoutObserver(interactor, metrics)
/**
* Start observing the [TabLayout] and select the current tab for initial state.
@ -46,13 +46,18 @@ class TabLayoutMediator(
internal fun selectActivePage() {
val selectedTab = browserStore.state.selectedTab ?: return
val selectedPagerPosition = if (selectedTab.content.private) {
POSITION_PRIVATE_TABS
} else {
POSITION_NORMAL_TABS
}
val selectedPagerPosition =
when (selectedTab.content.private) {
true -> POSITION_PRIVATE_TABS
false -> POSITION_NORMAL_TABS
}
selectTabAtPosition(selectedPagerPosition)
}
tabLayout.getTabAt(selectedPagerPosition)?.select()
fun selectTabAtPosition(position: Int) {
tabLayout.getTabAt(position)?.select()
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(position)))
}
}
@ -61,7 +66,6 @@ class TabLayoutMediator(
*/
internal class TabLayoutObserver(
private val interactor: TabsTrayInteractor,
private val trayStore: TabsTrayStore,
private val metrics: MetricController
) : TabLayout.OnTabSelectedListener {
@ -78,9 +82,7 @@ internal class TabLayoutObserver(
interactor.setCurrentTrayPosition(tab.position, animate)
trayStore.dispatch(TabsTrayAction.PageSelected(tab.toPage()))
Do exhaustive when (tab.toPage()) {
Do exhaustive when (Page.positionToPage(tab.position)) {
Page.NormalTabs -> metrics.track(Event.TabsTrayNormalModeTapped)
Page.PrivateTabs -> metrics.track(Event.TabsTrayPrivateModeTapped)
Page.SyncedTabs -> metrics.track(Event.TabsTraySyncedModeTapped)
@ -90,9 +92,3 @@ internal class TabLayoutObserver(
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
override fun onTabReselected(tab: TabLayout.Tab) = Unit
}
fun TabLayout.Tab.toPage() = when (this.position) {
0 -> Page.NormalTabs
1 -> Page.PrivateTabs
else -> Page.SyncedTabs
}

@ -28,8 +28,11 @@ import kotlinx.android.synthetic.main.tabstray_multiselect_items.*
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.concept.tabstray.Tab
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.HomeActivity
@ -47,8 +50,10 @@ import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.SelectionHandleBinding
import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding
import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding.VisibilityModifier
import org.mozilla.fenix.tabstray.ext.getTrayPosition
import org.mozilla.fenix.tabstray.ext.showWithTheme
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
import org.mozilla.fenix.utils.allowUndo
import kotlin.math.max
@Suppress("TooManyFunctions", "LargeClass")
@ -188,7 +193,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
tabLayout = tab_layout,
interactor = this,
browserStore = requireComponents.core.store,
trayStore = tabsTrayStore,
tabsTrayStore = tabsTrayStore,
metrics = requireComponents.analytics.metrics
), owner = this,
view = view
@ -262,6 +267,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
override fun setCurrentTrayPosition(position: Int, smoothScroll: Boolean) {
tabsTray.setCurrentItem(position, smoothScroll)
tab_layout.getTabAt(position)?.select()
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(position)))
}
override fun navigateToBrowser() {
@ -279,12 +286,15 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}
override fun onDeleteTab(tabId: String) {
// TODO re-implement these methods
// showUndoSnackbarForTab(sessionId)
// removeIfNotLastTab(sessionId)
// Temporary
requireComponents.useCases.tabsUseCases.removeTab(tabId)
val browserStore = requireComponents.core.store
val tab = browserStore.state.findTab(tabId)
tab?.let {
requireComponents.useCases.tabsUseCases.removeTab(tabId)
if (browserStore.state.getNormalOrPrivateTabs(it.content.private).isNotEmpty()) {
showUndoSnackbarForTab(it)
}
}
}
override fun onDeleteTabs(tabs: Collection<Tab>) {
@ -293,6 +303,27 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}
}
private fun showUndoSnackbarForTab(removedTab: TabSessionState) {
val snackbarMessage =
when (removedTab.content.private) {
true -> getString(R.string.snackbar_private_tab_closed)
false -> getString(R.string.snackbar_tab_closed)
}
lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
requireComponents.useCases.tabsUseCases.undo.invoke()
tabLayoutMediator.withFeature { it.selectTabAtPosition(removedTab.getTrayPosition()) }
},
operation = { },
elevation = ELEVATION,
anchorView = new_tab_button
)
}
private fun setupPager(
context: Context,
store: TabsTrayStore,
@ -355,5 +386,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
// Minimum number of grid items for which to show the tabs tray as expanded.
private const val EXPAND_AT_GRID_SIZE = 3
// Elevation for undo toasts
private const val ELEVATION = 80f
}
}

@ -66,7 +66,17 @@ enum class Page {
/**
* The pager position that displays Synced Tabs.
*/
SyncedTabs
SyncedTabs;
companion object {
fun positionToPage(position: Int): Page {
return when (position) {
0 -> NormalTabs
1 -> PrivateTabs
else -> SyncedTabs
}
}
}
}
/**

@ -5,6 +5,7 @@
package org.mozilla.fenix.tabstray.ext
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList.BrowserTabType.PRIVATE
import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList.Configuration
@ -13,3 +14,9 @@ fun TabSessionState.filterFromConfig(configuration: Configuration): Boolean {
return content.private == isPrivate
}
fun TabSessionState.getTrayPosition(): Int =
when (content.private) {
true -> Page.NormalTabs.ordinal
false -> Page.NormalTabs.ordinal
}

@ -17,6 +17,7 @@ import org.junit.runner.RunWith
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_NORMAL_TABS
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_PRIVATE_TABS
import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_SYNCED_TABS
@RunWith(FenixRobolectricTestRunner::class)
class TabLayoutMediatorTest {
@ -24,40 +25,71 @@ class TabLayoutMediatorTest {
@Test
fun `page to normal tab position when selected tab is also normal`() {
val store = createStore("123")
val tabsTrayStore: TabsTrayStore = mockk(relaxed = true)
val tabLayout: TabLayout = mockk(relaxed = true)
val tab: TabLayout.Tab = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, mockk(), mockk())
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, tabsTrayStore, mockk())
every { tabLayout.getTabAt(POSITION_NORMAL_TABS) }.answers { tab }
mediator.selectActivePage()
verify { tab.select() }
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_NORMAL_TABS))) }
}
@Test
fun `page to private tab position when selected tab is also private`() {
val store = createStore("456")
val tabsTrayStore: TabsTrayStore = mockk(relaxed = true)
val tabLayout: TabLayout = mockk(relaxed = true)
val tab: TabLayout.Tab = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, mockk(), mockk())
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, tabsTrayStore, mockk())
every { tabLayout.getTabAt(POSITION_PRIVATE_TABS) }.answers { tab }
mediator.selectActivePage()
verify { tab.select() }
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_PRIVATE_TABS))) }
}
@Test
fun `selectTabAtPosition will dispatch the correct TabsTrayStore action`() {
val store = createStore("456")
val tabsTrayStore: TabsTrayStore = mockk(relaxed = true)
val tabLayout: TabLayout = mockk(relaxed = true)
val tab: TabLayout.Tab = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, tabsTrayStore, mockk())
every { tabLayout.getTabAt(POSITION_NORMAL_TABS) }.answers { tab }
every { tabLayout.getTabAt(POSITION_PRIVATE_TABS) }.answers { tab }
every { tabLayout.getTabAt(POSITION_SYNCED_TABS) }.answers { tab }
mediator.selectTabAtPosition(POSITION_NORMAL_TABS)
verify { tab.select() }
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_NORMAL_TABS))) }
mediator.selectTabAtPosition(POSITION_PRIVATE_TABS)
verify { tab.select() }
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_PRIVATE_TABS))) }
mediator.selectTabAtPosition(POSITION_SYNCED_TABS)
verify { tab.select() }
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_SYNCED_TABS))) }
}
@Test
fun `lifecycle methods adds and removes observer`() {
val store = createStore("456")
val tabsTrayStore: TabsTrayStore = mockk(relaxed = true)
val tabLayout: TabLayout = mockk(relaxed = true)
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, mockk(), mockk())
val mediator = TabLayoutMediator(tabLayout, mockk(relaxed = true), store, tabsTrayStore, mockk())
mediator.start()
verify { tabLayout.addOnTabSelectedListener(any()) }
verify { tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(POSITION_PRIVATE_TABS))) }
mediator.stop()

@ -10,7 +10,6 @@ import io.mockk.mockk
import io.mockk.verify
import mozilla.components.support.test.libstate.ext.waitUntilIdle
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.components.metrics.Event
@ -30,7 +29,7 @@ class TabLayoutObserverTest {
@Test
fun `WHEN tab is selected THEN notify the interactor`() {
val observer = TabLayoutObserver(interactor, store, metrics)
val observer = TabLayoutObserver(interactor, metrics)
val tab = mockk<TabLayout.Tab>()
every { tab.position } returns 1
@ -41,10 +40,6 @@ class TabLayoutObserverTest {
verify { interactor.setCurrentTrayPosition(1, false) }
verify { metrics.track(Event.TabsTrayPrivateModeTapped) }
middleware.assertLastAction(TabsTrayAction.PageSelected::class) {
assertTrue(it.page == Page.PrivateTabs)
}
every { tab.position } returns 0
observer.onTabSelected(tab)
@ -54,10 +49,6 @@ class TabLayoutObserverTest {
verify { interactor.setCurrentTrayPosition(0, true) }
verify { metrics.track(Event.TabsTrayNormalModeTapped) }
middleware.assertLastAction(TabsTrayAction.PageSelected::class) {
assertTrue(it.page == Page.NormalTabs)
}
every { tab.position } returns 2
observer.onTabSelected(tab)
@ -66,16 +57,11 @@ class TabLayoutObserverTest {
verify { interactor.setCurrentTrayPosition(2, true) }
verify { metrics.track(Event.TabsTraySyncedModeTapped) }
middleware.assertLastAction(TabsTrayAction.PageSelected::class) {
assertTrue(it.page == Page.SyncedTabs)
}
}
@Test
fun `WHEN observer is first started THEN do not smooth scroll`() {
val store = TabsTrayStore()
val observer = TabLayoutObserver(interactor, store, metrics)
val observer = TabLayoutObserver(interactor, metrics)
val tab = mockk<TabLayout.Tab>()
every { tab.position } returns 1

@ -103,6 +103,15 @@ class TabsTrayStoreTest {
assertEquals(Page.SyncedTabs, store.state.selectedPage)
}
@Test
fun `WHEN position is converted to page THEN page is correct`() {
assert(Page.positionToPage(0) == Page.NormalTabs)
assert(Page.positionToPage(1) == Page.PrivateTabs)
assert(Page.positionToPage(2) == Page.SyncedTabs)
assert(Page.positionToPage(3) == Page.SyncedTabs)
assert(Page.positionToPage(-1) == Page.SyncedTabs)
}
@Test
fun `WHEN sync now action is triggered THEN update the sync now boolean`() {
val store = TabsTrayStore()

Loading…
Cancel
Save