Skip to content

Commit

Permalink
For mozilla-mobile#19916 - Add last viewed tab to home screen
Browse files Browse the repository at this point in the history
Co-authored-by: Jonathan Almeida <jalmeida@mozilla.com>
  • Loading branch information
gabrielluong and jonalmeida committed Jun 10, 2021
1 parent 0c1e712 commit e686281
Show file tree
Hide file tree
Showing 29 changed files with 837 additions and 26 deletions.
5 changes: 5 additions & 0 deletions app/src/main/java/org/mozilla/fenix/FeatureFlags.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ object FeatureFlags {
* Enables the Home button in the browser toolbar to navigate back to the home screen.
*/
val showHomeButtonFeature = Config.channel.isNightlyOrDebug

/**
* Enables the "recent" tabs feature in the home screen.
*/
val showRecentTabsFeature = Config.channel.isNightlyOrDebug
}
24 changes: 24 additions & 0 deletions app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* 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.ext

import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState

/**
* Returns the currently selected tab if there's one as a list.
*
* @return A list of the currently selected tab or an empty list.
*/
fun BrowserState.asRecentTabs(): List<TabSessionState> {
val tab = selectedTab

return if (tab != null && !tab.content.private) {
listOfNotNull(tab)
} else {
emptyList()
}
}
28 changes: 25 additions & 3 deletions app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
Expand All @@ -101,6 +102,7 @@ import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.components.tips.providers.MasterPasswordTipProvider
import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
Expand All @@ -111,7 +113,9 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.recenttabs.controller.DefaultRecentTabsController
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.RecentTabsListFeature
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
Expand Down Expand Up @@ -171,6 +175,7 @@ class HomeFragment : Fragment() {
private lateinit var currentMode: CurrentMode

private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
private val recentTabsListFeature = ViewBoundFeatureWrapper<RecentTabsListFeature>()

@VisibleForTesting
internal var getMenuButton: () -> MenuButton? = { menuButton }
Expand Down Expand Up @@ -228,7 +233,8 @@ class HomeFragment : Fragment() {
).getTip()
},
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard()
showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard(),
recentTabs = components.core.store.state.asRecentTabs()
)
)
}
Expand All @@ -243,8 +249,19 @@ class HomeFragment : Fragment() {
view = view
)

if (FeatureFlags.showRecentTabsFeature) {
recentTabsListFeature.set(
feature = RecentTabsListFeature(
browserStore = components.core.store,
homeStore = homeFragmentStore
),
owner = viewLifecycleOwner,
view = view
)
}

_sessionControlInteractor = SessionControlInteractor(
DefaultSessionControlController(
controller = DefaultSessionControlController(
activity = activity,
settings = components.settings,
engine = components.core.engine,
Expand All @@ -263,6 +280,10 @@ class HomeFragment : Fragment() {
showDeleteCollectionPrompt = ::showDeleteCollectionPrompt,
showTabTray = ::openTabsTray,
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel
),
recentTabController = DefaultRecentTabsController(
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
navController = findNavController()
)
)

Expand Down Expand Up @@ -577,7 +598,8 @@ class HomeFragment : Fragment() {
)
).getTip()
},
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome
showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
recentTabs = components.core.store.state.asRecentTabs()
)
)

Expand Down
25 changes: 16 additions & 9 deletions app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package org.mozilla.fenix.home

import android.graphics.Bitmap
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.state.Action
Expand All @@ -16,7 +17,7 @@ import org.mozilla.fenix.components.tips.Tip
* The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s.
*/
class HomeFragmentStore(
initialState: HomeFragmentState
initialState: HomeFragmentState = HomeFragmentState()
) : Store<HomeFragmentState, HomeFragmentAction>(
initialState, ::homeFragmentStateReducer
)
Expand All @@ -41,15 +42,17 @@ data class Tab(
* @property topSites The list of [TopSite] in the [HomeFragment].
* @property tip The current [Tip] to show on the [HomeFragment].
* @property showCollectionPlaceholder If true, shows a placeholder when there are no collections.
* @property recentTabs The list of recent [TabSessionState] in the [HomeFragment].
*/
data class HomeFragmentState(
val collections: List<TabCollection>,
val expandedCollections: Set<Long>,
val mode: Mode,
val topSites: List<TopSite>,
val collections: List<TabCollection> = emptyList(),
val expandedCollections: Set<Long> = emptySet(),
val mode: Mode = Mode.Normal,
val topSites: List<TopSite> = emptyList(),
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean,
val showSetAsDefaultBrowserCard: Boolean
val showCollectionPlaceholder: Boolean = false,
val showSetAsDefaultBrowserCard: Boolean = false,
val recentTabs: List<TabSessionState> = emptyList()
) : State

sealed class HomeFragmentAction : Action {
Expand All @@ -58,7 +61,8 @@ sealed class HomeFragmentAction : Action {
val mode: Mode,
val collections: List<TabCollection>,
val tip: Tip? = null,
val showCollectionPlaceholder: Boolean
val showCollectionPlaceholder: Boolean,
val recentTabs: List<TabSessionState>
) :
HomeFragmentAction()

Expand All @@ -69,6 +73,7 @@ sealed class HomeFragmentAction : Action {
data class ModeChange(val mode: Mode) : HomeFragmentAction()
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction()
data class RecentTabsChange(val recentTabs: List<TabSessionState>) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction()
object RemoveSetDefaultBrowserCard : HomeFragmentAction()
}
Expand All @@ -82,7 +87,8 @@ private fun homeFragmentStateReducer(
collections = action.collections,
mode = action.mode,
topSites = action.topSites,
tip = action.tip
tip = action.tip,
recentTabs = action.recentTabs
)
is HomeFragmentAction.CollectionExpanded -> {
val newExpandedCollection = state.expandedCollections.toMutableSet()
Expand All @@ -105,5 +111,6 @@ private fun homeFragmentStateReducer(
state.copy(showCollectionPlaceholder = false)
}
is HomeFragmentAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false)
is HomeFragmentAction.RecentTabsChange -> state.copy(recentTabs = action.recentTabs)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* 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.home.recenttabs.controller

import androidx.navigation.NavController
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.recenttabs.interactor.RecentTabInteractor

/**
* An interface that handles the view manipulation of the recent tabs in the Home screen.
*/
interface RecentTabController {

/**
* @see [RecentTabInteractor.onRecentTabClicked]
*/
fun handleRecentTabClicked(tabId: String)

/**
* @see [RecentTabInteractor.onRecentTabShowAllClicked]
*/
fun handleRecentTabShowAllClicked()
}

/**
* The default implementation of [RecentTabController].
*/
class DefaultRecentTabsController(
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val navController: NavController
) : RecentTabController {

override fun handleRecentTabClicked(tabId: String) {
selectTabUseCase.invoke(tabId)
navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}

override fun handleRecentTabShowAllClicked() {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalTabsTrayFragment()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* 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.home.recenttabs.interactor

/**
* Interface for recent tab related actions in the Home screen.
*/
interface RecentTabInteractor {
/**
* Opens the given tab. Called when a user clicks on a recent tab.
*
* @param tabId The ID of the tab to open.
*/
fun onRecentTabClicked(tabId: String)

/**
* Show the tabs tray. Called when a user clicks on the "Show all" button besides the recent
* tabs.
*/
fun onRecentTabShowAllClicked()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* 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.home.recenttabs.view

import android.view.View
import kotlinx.android.synthetic.main.recent_tabs_list_row.*
import mozilla.components.browser.state.state.TabSessionState
import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.utils.view.ViewHolder

/**
* View holder for a recent tab item.
*/
class RecentTabViewHolder(
view: View,
private val interactor: SessionControlInteractor
) : ViewHolder(view) {

fun bindTab(tab: TabSessionState) {
recent_tab_title.text = tab.content.title
recent_tab_icon.setImageBitmap(tab.content.icon)

itemView.setOnClickListener {
interactor.onRecentTabClicked(tab.id)
}
}

companion object {
const val LAYOUT_ID = R.layout.recent_tabs_list_row
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* 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.home.recenttabs.view

import android.view.View
import kotlinx.android.synthetic.main.recent_tabs_header.*
import org.mozilla.fenix.R
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.utils.view.ViewHolder

/**
* View holder for the recent tabs header and "Show all" button.
*/
class RecentTabsHeaderViewHolder(
view: View,
private val interactor: SessionControlInteractor
) : ViewHolder(view) {

init {
show_all_button.setOnClickListener {
interactor.onRecentTabShowAllClicked()
}
}

companion object {
const val LAYOUT_ID = R.layout.recent_tabs_header
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* 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.home.sessioncontrol

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore

/**
* View-bound feature that dispatches recent tab changes to the [HomeFragmentStore] when the
* [BrowserStore] is updated.
*/
@OptIn(ExperimentalCoroutinesApi::class)
class RecentTabsListFeature(
private val browserStore: BrowserStore,
private val homeStore: HomeFragmentStore
) : AbstractBinding<BrowserState>(browserStore) {

override suspend fun onState(flow: Flow<BrowserState>) {
flow.map { it.selectedTabId }
.ifChanged()
.collect { selectedTabId ->
// Attempt to get the selected normal tab since here may not be a selected tab or
// the selected tab may be a private tab.
val selectedTab = browserStore.state.normalTabs.firstOrNull {
it.id == selectedTabId
}
val recentTabsList = if (selectedTab != null) {
listOf(selectedTab)
} else {
emptyList()
}

homeStore.dispatch(HomeFragmentAction.RecentTabsChange(recentTabsList))
}
}
}
Loading

0 comments on commit e686281

Please sign in to comment.