diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt index 031a6eacc..a36f14975 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt @@ -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 -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index cfc6c8360..1c0f76bd8 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -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) { @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt index 283f44764..28c9da2f2 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt @@ -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 + } + } + } } /** diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt index cf11c8c39..777e4f47e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt @@ -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 + } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutMediatorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutMediatorTest.kt index b6bf0142c..2695a0467 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutMediatorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutMediatorTest.kt @@ -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() diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutObserverTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutObserverTest.kt index df4c2ff8a..e9c2f2922 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutObserverTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabLayoutObserverTest.kt @@ -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() 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() every { tab.position } returns 1 diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt index a84ff3344..856818b80 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreTest.kt @@ -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()