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 { bundleArgs.getString(SESSION_TO_DELETE)?.also {
sessionManager.findSessionById(it)?.let { session -> if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
val snapshot = sessionManager.createSessionSnapshot(session) removeAllTabsAndShowSnackbar(it)
val state = snapshot.engineSession?.saveState() } else {
val isSelected = removeTabAndShowSnackbar(it)
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)
} }
} }
updateTabCounter(requireComponents.core.store.state) 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() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
_sessionControlInteractor = null _sessionControlInteractor = null
@ -888,6 +934,9 @@ class HomeFragment : Fragment() {
} }
companion object { 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 FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
private const val SESSION_TO_DELETE = "session_to_delete" private const val SESSION_TO_DELETE = "session_to_delete"
private const val ANIMATION_DELAY = 100L private const val ANIMATION_DELAY = 100L

View File

@ -6,8 +6,8 @@ package org.mozilla.fenix.tabtray
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R 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.collections.SaveCollectionStep
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment
/** /**
* [TabTrayDialogFragment] controller. * [TabTrayDialogFragment] controller.
@ -34,7 +35,7 @@ class DefaultTabTrayController(
private val activity: HomeActivity, private val activity: HomeActivity,
private val navController: NavController, private val navController: NavController,
private val dismissTabTray: () -> Unit, private val dismissTabTray: () -> Unit,
private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit, private val dismissTabTrayAndNavigateHome: (String) -> Unit,
private val registerCollectionStorageObserver: () -> Unit private val registerCollectionStorageObserver: () -> Unit
) : TabTrayController { ) : TabTrayController {
override fun onNewTabTapped(private: Boolean) { override fun onNewTabTapped(private: Boolean) {
@ -89,35 +90,15 @@ class DefaultTabTrayController(
navController.navigate(directions) navController.navigate(directions)
} }
@OptIn(ExperimentalCoroutinesApi::class)
override fun onCloseAllTabsClicked(private: Boolean) { override fun onCloseAllTabsClicked(private: Boolean) {
val sessionManager = activity.components.core.sessionManager val sessionsToClose = if (private) {
val tabs = getListOfSessions(private) HomeFragment.ALL_PRIVATE_TABS
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)
} else { } else {
activity.getString(R.string.snackbar_tabs_closed) HomeFragment.ALL_NORMAL_TABS
} }
showUndoSnackbar(snackbarMessage, snapshot) dismissTabTrayAndNavigateHome(sessionsToClose)
dismissTabTray()
} }
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @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.android.synthetic.main.fragment_tab_tray_dialog.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session 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.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases 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 mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
@ -82,13 +78,24 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
override fun invoke(sessionId: String) { override fun invoke(sessionId: String) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(sessionId) showUndoSnackbarForTab(sessionId)
requireComponents.useCases.tabsUseCases.removeTab(sessionId) removeIfNotLastTab(sessionId)
} }
override fun invoke(session: Session) { override fun invoke(session: Session) {
requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) requireContext().components.analytics.metrics.track(Event.ClosedExistingTab)
showUndoSnackbarForTab(session.id) 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), activity = (activity as HomeActivity),
navController = findNavController(), navController = findNavController(),
dismissTabTray = ::dismissAllowingStateLoss, dismissTabTray = ::dismissAllowingStateLoss,
showUndoSnackbar = ::showUndoSnackbar, dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
registerCollectionStorageObserver = ::registerCollectionStorageObserver registerCollectionStorageObserver = ::registerCollectionStorageObserver
) )
), ),
@ -177,7 +184,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
consumeFrom(requireComponents.core.store) { consumeFrom(requireComponents.core.store) {
tabTrayView.updateState(it) tabTrayView.updateState(it)
navigateHomeIfNeeded(it)
} }
} }
@ -191,11 +197,20 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
private fun showUndoSnackbarForTab(sessionId: String) { private fun showUndoSnackbarForTab(sessionId: String) {
val sessionManager = view?.context?.components?.core?.sessionManager val sessionManager = view?.context?.components?.core?.sessionManager
val snapshot = sessionManager val snapshot = sessionManager
?.findSessionById(sessionId)?.let { ?.findSessionById(sessionId)?.let {
sessionManager.createSessionSnapshot(it) sessionManager.createSessionSnapshot(it)
} ?: return } ?: 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 state = snapshot.engineSession?.saveState()
val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false val isSelected = sessionId == requireComponents.core.store.state.selectedTabId ?: false
@ -205,13 +220,8 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
getString(R.string.snackbar_tab_closed) getString(R.string.snackbar_tab_closed)
} }
// Check if this is the last tab of this session type lifecycleScope.allowUndo(
val isLastOpenTab = sessionManager.sessions.filter { snapshot.session.private == it.private }.size == 1 requireView().tabLayout,
val rootView = if (isLastOpenTab) { requireActivity().getRootView()!! } else { requireView().tabLayout }
val anchorView = if (isLastOpenTab) { null } else { snackbarAnchor }
requireActivity().lifecycleScope.allowUndo(
rootView,
snackbarMessage, snackbarMessage,
getString(R.string.snackbar_deleted_undo), getString(R.string.snackbar_deleted_undo),
{ {
@ -220,18 +230,14 @@ class TabTrayDialogFragment : AppCompatDialogFragment() {
}, },
operation = { }, operation = { },
elevation = ELEVATION, elevation = ELEVATION,
paddedForBottomToolbar = isLastOpenTab, anchorView = snackbarAnchor
anchorView = anchorView
) )
dismissTabTrayIfNecessary()
} }
private fun dismissTabTrayIfNecessary() { private fun dismissTabTrayAndNavigateHome(sessionId: String) {
if (requireComponents.core.sessionManager.sessions.size == 1) { val directions = BrowserFragmentDirections.actionGlobalHome(sessionToDelete = sessionId)
findNavController().popBackStack(R.id.homeFragment, false) findNavController().navigate(directions)
dismissAllowingStateLoss() dismissAllowingStateLoss()
}
} }
override fun onDestroyView() { 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() { private fun registerCollectionStorageObserver() {
requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) 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) { private fun showCollectionSnackbar(tabSize: Int, isNewCollection: Boolean = false) {
view.let { view.let {
val messageStringRes = when { val messageStringRes = when {

View File

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