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 08d9d06a1..f51454c9c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt @@ -4,11 +4,13 @@ package org.mozilla.fenix.tabstray +import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.TabSessionState 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.browser.TabGroup /** * Value type that represents the state of the tabs tray. @@ -16,13 +18,20 @@ import mozilla.components.lib.state.Store * @property selectedPage The current page in the tray can be on. * @property mode Whether the browser tab list is in multi-select mode or not with the set of * currently selected tabs. - * @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired - * devices. + * @property inactiveTabs The list of tabs are considered inactive. + * @property searchTermGroups The list of tab groups. + * @property normalTabs The list of normal tabs that do not fall under [inactiveTabs] or [searchTermGroups]. + * @property privateTabs The list of tabs that are [ContentState.private]. + * @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired devices. * @property focusGroupTabId The search group tab id to focus. Defaults to null. */ data class TabsTrayState( val selectedPage: Page = Page.NormalTabs, val mode: Mode = Mode.Normal, + val inactiveTabs: List = emptyList(), + val searchTermGroups: List = emptyList(), + val normalTabs: List = emptyList(), + val privateTabs: List = emptyList(), val syncing: Boolean = false, val focusGroupTabId: String? = null ) : State { @@ -126,6 +135,26 @@ sealed class TabsTrayAction : Action { * Removes the [TabsTrayState.focusGroupTabId] of the [TabsTrayState]. */ object ConsumeFocusGroupTabIdAction : TabsTrayAction() + + /** + * Updates the list of tabs in [TabsTrayState.inactiveTabs]. + */ + data class UpdateInactiveTabs(val tabs: List) : TabsTrayAction() + + /** + * Updates the list of tab groups in [TabsTrayState.searchTermGroups]. + */ + data class UpdateSearchGroupTabs(val groups: List) : TabsTrayAction() + + /** + * Updates the list of tabs in [TabsTrayState.normalTabs]. + */ + data class UpdateNormalTabs(val tabs: List) : TabsTrayAction() + + /** + * Updates the list of tabs in [TabsTrayState.privateTabs]. + */ + data class UpdatePrivateTabs(val tabs: List) : TabsTrayAction() } /** @@ -158,6 +187,14 @@ internal object TabsTrayReducer { state.copy(syncing = false) is TabsTrayAction.ConsumeFocusGroupTabIdAction -> state.copy(focusGroupTabId = null) + is TabsTrayAction.UpdateInactiveTabs -> + state.copy(inactiveTabs = action.tabs) + is TabsTrayAction.UpdateSearchGroupTabs -> + state.copy(searchTermGroups = action.groups) + is TabsTrayAction.UpdateNormalTabs -> + state.copy(normalTabs = action.tabs) + is TabsTrayAction.UpdatePrivateTabs -> + state.copy(privateTabs = action.tabs) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt index 875a62e78..f85acb80d 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabSorter.kt @@ -12,6 +12,8 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.maxActiveTime import org.mozilla.fenix.ext.toSearchGroup +import org.mozilla.fenix.tabstray.TabsTrayAction +import org.mozilla.fenix.tabstray.TabsTrayStore import org.mozilla.fenix.tabstray.ext.browserAdapter import org.mozilla.fenix.tabstray.ext.hasSearchTerm import org.mozilla.fenix.tabstray.ext.inactiveTabsAdapter @@ -27,18 +29,23 @@ import org.mozilla.fenix.utils.Settings class TabSorter( private val settings: Settings, private val metrics: MetricController, - private val concatAdapter: ConcatAdapter + private val concatAdapter: ConcatAdapter? = null, + private val tabsTrayStore: TabsTrayStore? = null ) : TabsTray { private var shouldReportMetrics: Boolean = true private val groupsSet = mutableSetOf() override fun updateTabs(tabs: List, selectedTabId: String?) { - val inactiveTabs = tabs.getInactiveTabs(settings) - val searchTermTabs = tabs.getSearchGroupTabs(settings) - val normalTabs = tabs - inactiveTabs - searchTermTabs + val privateTabs = tabs.filter { it.content.private } + tabsTrayStore?.dispatch(TabsTrayAction.UpdatePrivateTabs(privateTabs)) + + val normalTabs = tabs - privateTabs + val inactiveTabs = normalTabs.getInactiveTabs(settings) + val searchTermTabs = normalTabs.getSearchGroupTabs(settings) + val regularTabs = normalTabs - inactiveTabs - searchTermTabs // Inactive tabs - concatAdapter.inactiveTabsAdapter.updateTabs(inactiveTabs, selectedTabId) + tabsTrayStore?.dispatch(TabsTrayAction.UpdateInactiveTabs(inactiveTabs)) // Tab groups // We don't need to provide a selectedId, because the [TabGroupAdapter] has that built-in with support from @@ -47,15 +54,17 @@ class TabSorter( groupsSet.clear() groupsSet.addAll(groups.map { it.searchTerm }) - concatAdapter.tabGroupAdapter.submitList(groups) + concatAdapter?.tabGroupAdapter?.submitList(groups) + tabsTrayStore?.dispatch(TabsTrayAction.UpdateSearchGroupTabs(groups)) // Normal tabs. - val totalNormalTabs = (normalTabs + remainderTabs) - concatAdapter.browserAdapter.updateTabs(totalNormalTabs, selectedTabId) + val totalNormalTabs = (regularTabs + remainderTabs) + concatAdapter?.browserAdapter?.updateTabs(totalNormalTabs, selectedTabId) + tabsTrayStore?.dispatch(TabsTrayAction.UpdateNormalTabs(totalNormalTabs)) // Normal tab title header. - concatAdapter.titleHeaderAdapter - .handleListChanges(totalNormalTabs.isNotEmpty() && groups.isNotEmpty()) + concatAdapter?.titleHeaderAdapter + ?.handleListChanges(totalNormalTabs.isNotEmpty() && groups.isNotEmpty()) if (shouldReportMetrics) { shouldReportMetrics = false diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt index 17421de2c..529c18a16 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayStoreReducerTest.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.tabstray +import mozilla.components.browser.state.state.createTab import org.junit.Assert.assertEquals import org.junit.Test @@ -20,4 +21,52 @@ class TabsTrayStoreReducerTest { assertEquals(expectedState, resultState) } + + @Test + fun `WHEN UpdateInactiveTabs THEN inactive tabs are added`() { + val inactiveTabs = listOf( + createTab("https://mozilla.org") + ) + val initialState = TabsTrayState() + val expectedState = initialState.copy(inactiveTabs = inactiveTabs) + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdateInactiveTabs(inactiveTabs) + ) + + assertEquals(expectedState, resultState) + } + + @Test + fun `WHEN UpdateNormalTabs THEN normal tabs are added`() { + val normalTabs = listOf( + createTab("https://mozilla.org") + ) + val initialState = TabsTrayState() + val expectedState = initialState.copy(normalTabs = normalTabs) + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdateNormalTabs(normalTabs) + ) + + assertEquals(expectedState, resultState) + } + + @Test + fun `WHEN UpdatePrivateTabs THEN private tabs are added`() { + val privateTabs = listOf( + createTab("https://mozilla.org", private = true) + ) + val initialState = TabsTrayState() + val expectedState = initialState.copy(privateTabs = privateTabs) + + val resultState = TabsTrayReducer.reduce( + initialState, + TabsTrayAction.UpdatePrivateTabs(privateTabs) + ) + + assertEquals(expectedState, resultState) + } } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt index 057c84ae8..d26baabed 100644 --- a/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabstray/browser/TabSorterTest.kt @@ -16,6 +16,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.tabstray.TabsTrayStore import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.INACTIVE_TABS_FEATURE_NAME import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.TABS_TRAY_FEATURE_NAME import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.TAB_GROUP_FEATURE_NAME @@ -31,6 +32,7 @@ class TabSorterTest { private val settings: Settings = mockk() private val metrics: MetricController = mockk() private var inactiveTimestamp = 0L + private val tabsTrayStore = TabsTrayStore() @Before fun setUp() { @@ -42,7 +44,7 @@ class TabSorterTest { @Test fun `WHEN updated with one normal tab THEN adapter have only one normal tab and no header`() { val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -66,7 +68,7 @@ class TabSorterTest { @Test fun `WHEN updated with one normal tab and two search term tab THEN adapter have normal tab and a search group`() { val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -102,7 +104,7 @@ class TabSorterTest { @Test fun `WHEN updated with one normal tab, one inactive tab and two search term tab THEN adapter have normal tab, inactive tab and a search group`() { val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -145,7 +147,7 @@ class TabSorterTest { fun `WHEN inactive tabs is off THEN adapter have no inactive tab`() { every { settings.inactiveTabsAreEnabled }.answers { false } val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -188,7 +190,7 @@ class TabSorterTest { fun `WHEN search term tabs is off THEN adapter have no search term group`() { every { settings.searchTermTabGroupsAreEnabled }.answers { false } val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -232,7 +234,7 @@ class TabSorterTest { every { settings.inactiveTabsAreEnabled }.answers { false } every { settings.searchTermTabGroupsAreEnabled }.answers { false } val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -273,7 +275,7 @@ class TabSorterTest { @Test fun `WHEN only one search term tab THEN there is no search group`() { val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME) @@ -300,7 +302,7 @@ class TabSorterTest { @Test fun `WHEN remove second last one search term tab THEN search group is kept even if there's only one tab`() { val adapter = ConcatAdapter( - InactiveTabsAdapter(context, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), + InactiveTabsAdapter(context, tabsTrayStore, mock(), mock(), INACTIVE_TABS_FEATURE_NAME, settings), TabGroupAdapter(context, mock(), mock(), TAB_GROUP_FEATURE_NAME), TitleHeaderAdapter(), BrowserTabsAdapter(context, mock(), mock(), TABS_TRAY_FEATURE_NAME)