1
0
Fork 0

For #12793: Improve snackbars for tabs tray

master
Sawyer Blatz 2020-07-22 10:57:41 -07:00
parent 6c58098fef
commit 9c56e1905b
4 changed files with 119 additions and 114 deletions

View File

@ -393,40 +393,86 @@ class HomeFragment : Fragment() {
}
bundleArgs.getString(SESSION_TO_DELETE)?.also {
sessionManager.findSessionById(it)?.let { session ->
val snapshot = sessionManager.createSessionSnapshot(session)
val state = snapshot.engineSession?.saveState()
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)
} else {
requireContext().getString(R.string.snackbar_tab_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(
snapshot.session,
isSelected,
engineSessionState = state
)
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null))
},
operation = { },
anchorView = snackbarAnchorView
)
requireComponents.useCases.tabsUseCases.removeTab.invoke(session)
if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
removeAllTabsAndShowSnackbar(it)
} else {
removeTabAndShowSnackbar(it)
}
}
updateTabCounter(requireComponents.core.store.state)
}
private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
val tabs = sessionManager.sessionsOfType(private = sessionCode == ALL_PRIVATE_TABS).toList()
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
.map {
it.copy(
engineSession = null,
engineSessionState = it.engineSession?.saveState()
)
}
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
}
val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
getString(R.string.snackbar_private_tabs_closed)
} else {
getString(R.string.snackbar_tabs_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.restore(snapshot)
},
operation = { },
anchorView = snackbarAnchorView
)
}
private fun removeTabAndShowSnackbar(sessionId: String) {
sessionManager.findSessionById(sessionId)?.let { session ->
val snapshot = sessionManager.createSessionSnapshot(session)
val state = snapshot.engineSession?.saveState()
val isSelected =
session.id == requireComponents.core.store.state.selectedTabId ?: false
sessionManager.remove(session)
val snackbarMessage = if (snapshot.session.private) {
requireContext().getString(R.string.snackbar_private_tab_closed)
} else {
requireContext().getString(R.string.snackbar_tab_closed)
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView(),
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
sessionManager.add(
snapshot.session,
isSelected,
engineSessionState = state
)
findNavController().navigate(HomeFragmentDirections.actionHomeFragmentToBrowserFragment(null))
},
operation = { },
anchorView = snackbarAnchorView
)
}
}
override fun onDestroyView() {
super.onDestroyView()
_sessionControlInteractor = null
@ -888,6 +934,9 @@ class HomeFragment : Fragment() {
}
companion object {
const val ALL_NORMAL_TABS = "all_normal"
const val ALL_PRIVATE_TABS = "all_private"
private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
private const val SESSION_TO_DELETE = "session_to_delete"
private const val ANIMATION_DELAY = 100L

View File

@ -6,8 +6,8 @@ package org.mozilla.fenix.tabtray
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.prompt.ShareData
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -15,6 +15,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment
/**
* [TabTrayDialogFragment] controller.
@ -34,7 +35,7 @@ class DefaultTabTrayController(
private val activity: HomeActivity,
private val navController: NavController,
private val dismissTabTray: () -> Unit,
private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit,
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
private val registerCollectionStorageObserver: () -> Unit
) : TabTrayController {
override fun onNewTabTapped(private: Boolean) {
@ -89,35 +90,15 @@ class DefaultTabTrayController(
navController.navigate(directions)
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) {
val sessionManager = activity.components.core.sessionManager
val tabs = getListOfSessions(private)
val selectedIndex = sessionManager
.selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0
val snapshot = tabs
.map(sessionManager::createSessionSnapshot)
.map {
it.copy(
engineSession = null,
engineSessionState = it.engineSession?.saveState()
)
}
.let { SessionManager.Snapshot(it, selectedIndex) }
tabs.forEach {
sessionManager.remove(it)
}
val snackbarMessage = if (private) {
activity.getString(R.string.snackbar_private_tabs_closed)
val sessionsToClose = if (private) {
HomeFragment.ALL_PRIVATE_TABS
} else {
activity.getString(R.string.snackbar_tabs_closed)
HomeFragment.ALL_NORMAL_TABS
}
showUndoSnackbar(snackbarMessage, snapshot)
dismissTabTray()
dismissTabTrayAndNavigateHome(sessionsToClose)
}
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)

View File

@ -22,10 +22,6 @@ import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
@ -34,11 +30,11 @@ import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.allowUndo
@ -82,13 +78,24 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
override fun invoke(sessionId: String) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(sessionId)
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
removeIfNotLastTab(sessionId)
}
override fun invoke(session: Session) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(session.id)
requireComponents.useCases.tabsUseCases.removeTab(session)
removeIfNotLastTab(session.id)
}
}
private fun removeIfNotLastTab(sessionId: String) {
// We only want to *immediately* remove a tab if there are more than one in the tab tray
// If there is only one, the HomeFragment handles deleting the tab (to better support snackbars)
val sessionManager = view?.context?.components?.core?.sessionManager
val sessionToRemove = sessionManager?.findSessionById(sessionId)
if (sessionManager?.sessions?.filter { sessionToRemove?.private == it.private }?.size != 1) {
requireComponents.useCases.tabsUseCases.removeTab(sessionId)
}
}
@ -127,7 +134,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
activity = (activity as HomeActivity),
navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss,
showUndoSnackbar = ::showUndoSnackbar,
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = ::registerCollectionStorageObserver
)
),
@ -177,7 +184,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
consumeFrom(requireComponents.core.store) {
tabTrayView.updateState(it)
navigateHomeIfNeeded(it)
}
}
@ -191,11 +197,20 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
private fun showUndoSnackbarForTab(sessionId: String) {
val sessionManager = view?.context?.components?.core?.sessionManager
val snapshot = sessionManager
?.findSessionById(sessionId)?.let {
sessionManager.createSessionSnapshot(it)
} ?: return
// Check if this is the last tab of this session type
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
if (isLastOpenTab) {
dismissTabTrayAndNavigateHome(sessionId)
return
}
val state = snapshot.engineSession?.saveState()
val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false
@ -205,13 +220,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
getString(R.string.snackbar_tab_closed)
}
// Check if this is the last tab of this session type
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1
val rootView = if (isLastOpenTab) { requireActivity().getRootView()!! } else { requireView().tabLayout }
val anchorView = if (isLastOpenTab) { null } else { snackbarAnchor }
requireActivity().lifecycleScope.allowUndo(
rootView,
lifecycleScope.allowUndo(
requireView().tabLayout,
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
@ -220,18 +230,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
},
operation = { },
elevation = ELEVATION,
paddedForBottomToolbar = isLastOpenTab,
anchorView = anchorView
anchorView = snackbarAnchor
)
dismissTabTrayIfNecessary()
}
private fun dismissTabTrayIfNecessary() {
if (requireComponents.core.sessionManager.sessions.size == 1) {
findNavController().popBackStack(R.id.homeFragment, false)
dismissAllowingStateLoss()
}
private fun dismissTabTrayAndNavigateHome(sessionId: String) {
val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId)
findNavController().navigate(directions)
dismissAllowingStateLoss()
}
override fun onDestroyView() {
@ -247,39 +253,10 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
}
}
private fun navigateHomeIfNeeded(state: BrowserState) {
val shouldPop = if (tabTrayView.isPrivateModeSelected) {
state.privateTabs.isEmpty()
} else {
state.normalTabs.isEmpty()
}
if (shouldPop) {
findNavController().popBackStack(R.id.homeFragment, false)
}
}
private fun registerCollectionStorageObserver() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this)
}
private fun showUndoSnackbar(snackbarMessage: String, snapshot: SessionManager.Snapshot) {
// Warning: removing this definition and using it directly in the onCancel block will fail silently.
val sessionManager = view?.context?.components?.core?.sessionManager
requireActivity().lifecycleScope.allowUndo(
requireActivity().getRootView()!!,
snackbarMessage,
getString(R.string.snackbar_deleted_undo),
{
sessionManager?.restore(snapshot)
},
operation = { },
elevation = ELEVATION,
paddedForBottomToolbar = true
)
}
private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
view.let {
val messageStringRes = when {

View File

@ -35,6 +35,7 @@ class DefaultTabTrayControllerTest {
private val navController: NavController = mockk()
private val sessionManager: SessionManager = mockk(relaxed = true)
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
private val showUndoSnackbar: ((String, SessionManager.Snapshot) -> Unit) =
mockk(relaxed = true)
private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true)
@ -78,7 +79,7 @@ class DefaultTabTrayControllerTest {
activity = activity,
navController = navController,
dismissTabTray = dismissTabTray,
showUndoSnackbar = showUndoSnackbar,
dismissTabTrayAndNavigateHome = dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = registerCollectionStorageObserver
)
}
@ -155,12 +156,9 @@ class DefaultTabTrayControllerTest {
@Test
fun onCloseAllTabsClicked() {
controller.onCloseAllTabsClicked(private = false)
val snackbarMessage = activity.getString(R.string.snackbar_tabs_closed)
verify {
sessionManager.createSessionSnapshot(nonPrivateSession)
sessionManager.remove(nonPrivateSession)
showUndoSnackbar(snackbarMessage, any())
dismissTabTrayAndNavigateHome(any())
}
}
}