Bug 1842250 - Refactor the remaining HistoryController functions to lib-state

fenix/119.0
Matthew Tighe 1 year ago committed by mergify[bot]
parent d2ff0bb65d
commit 816dbfdead

@ -63,6 +63,7 @@ import org.mozilla.fenix.library.history.state.HistoryNavigationMiddleware
import org.mozilla.fenix.library.history.state.HistoryStorageMiddleware import org.mozilla.fenix.library.history.state.HistoryStorageMiddleware
import org.mozilla.fenix.library.history.state.HistorySyncMiddleware import org.mozilla.fenix.library.history.state.HistorySyncMiddleware
import org.mozilla.fenix.library.history.state.HistoryTelemetryMiddleware import org.mozilla.fenix.library.history.state.HistoryTelemetryMiddleware
import org.mozilla.fenix.library.history.state.bindings.MenuBinding
import org.mozilla.fenix.library.history.state.bindings.PendingDeletionBinding import org.mozilla.fenix.library.history.state.bindings.PendingDeletionBinding
import org.mozilla.fenix.tabstray.Page import org.mozilla.fenix.tabstray.Page
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
@ -95,6 +96,13 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
PendingDeletionBinding(requireContext().components.appStore, historyView) PendingDeletionBinding(requireContext().components.appStore, historyView)
} }
private val menuBinding by lazy {
MenuBinding(
store = historyStore,
invalidateOptionsMenu = { activity?.invalidateOptionsMenu() },
)
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -110,6 +118,7 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
HistoryNavigationMiddleware( HistoryNavigationMiddleware(
navController = findNavController(), navController = findNavController(),
openToBrowser = ::openItem, openToBrowser = ::openItem,
onBackPressed = requireActivity().onBackPressedDispatcher::onBackPressed,
), ),
HistoryTelemetryMiddleware( HistoryTelemetryMiddleware(
isInPrivateMode = requireComponents.appStore.state.mode == Mode.Private, isInPrivateMode = requireComponents.appStore.state.mode == Mode.Private,
@ -263,10 +272,12 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
private fun startStateBindings() { private fun startStateBindings() {
pendingDeletionBinding.start() pendingDeletionBinding.start()
menuBinding.start()
} }
private fun stopStateBindings() { private fun stopStateBindings() {
pendingDeletionBinding.stop() pendingDeletionBinding.stop()
menuBinding.stop()
} }
private fun updateDeleteMenuItemView(isEnabled: Boolean) { private fun updateDeleteMenuItemView(isEnabled: Boolean) {
@ -414,8 +425,12 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
} }
} }
override fun onBackPressed(): Boolean { override fun onBackPressed() = if (FeatureFlags.historyFragmentLibStateRefactor) {
return historyView.onBackPressed() // The state needs to be updated accordingly if Edit mode is active
historyStore.dispatch(HistoryFragmentAction.BackPressed)
true
} else {
historyView.onBackPressed()
} }
override fun onDestroyView() { override fun onDestroyView() {

@ -149,6 +149,16 @@ sealed class HistoryFragmentAction : Action {
*/ */
data class DeleteItems(val items: Set<History>) : HistoryFragmentAction() data class DeleteItems(val items: Set<History>) : HistoryFragmentAction()
/**
* The user has clicked to enter the Recently Closed fragment.
*/
object EnterRecentlyClosed : HistoryFragmentAction()
/**
* A back press event has been dispatched.
*/
object BackPressed : HistoryFragmentAction()
/** /**
* Updates the empty state of [org.mozilla.fenix.library.history.HistoryView]. * Updates the empty state of [org.mozilla.fenix.library.history.HistoryView].
*/ */
@ -259,10 +269,18 @@ private fun historyStateReducer(
) )
} }
} }
is HistoryFragmentAction.BackPressed -> {
if (state.mode is HistoryFragmentState.Mode.Editing) {
state.copy(mode = HistoryFragmentState.Mode.Normal)
} else {
state
}
}
// For deletion actions: the item list is handled through storage. // For deletion actions: the item list is handled through storage.
// Updates from storage are dispatched directly to the view. // Updates from storage are dispatched directly to the view.
is HistoryFragmentAction.DeleteItems, is HistoryFragmentAction.DeleteItems,
is HistoryFragmentAction.DeleteTimeRange, is HistoryFragmentAction.DeleteTimeRange,
is HistoryFragmentAction.EnterRecentlyClosed,
-> state -> state
} }
} }

@ -99,7 +99,7 @@ class HistoryView(
val last = layoutManager.findLastVisibleItemPosition() + 1 val last = layoutManager.findLastVisibleItemPosition() + 1
historyAdapter.notifyItemRangeChanged(first, last - first) historyAdapter.notifyItemRangeChanged(first, last - first)
if (state.mode::class != oldMode::class) { if (state.mode::class != oldMode::class && !FeatureFlags.historyFragmentLibStateRefactor) {
interactor.onModeSwitched() interactor.onModeSwitched()
} }
@ -135,7 +135,11 @@ class HistoryView(
with(binding.recentlyClosedNavEmpty) { with(binding.recentlyClosedNavEmpty) {
recentlyClosedNav.setOnClickListener { recentlyClosedNav.setOnClickListener {
interactor.onRecentlyClosedClicked() if (FeatureFlags.historyFragmentLibStateRefactor) {
store.dispatch(HistoryFragmentAction.EnterRecentlyClosed)
} else {
interactor.onRecentlyClosedClicked()
}
} }
val numRecentTabs = recentlyClosedNav.context.components.core.store.state.closedTabs.size val numRecentTabs = recentlyClosedNav.context.components.core.store.state.closedTabs.size
recentlyClosedTabsDescription.text = String.format( recentlyClosedTabsDescription.text = String.format(

@ -27,6 +27,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentState
class HistoryNavigationMiddleware( class HistoryNavigationMiddleware(
private val navController: NavController, private val navController: NavController,
private val openToBrowser: (item: History.Regular) -> Unit, private val openToBrowser: (item: History.Regular) -> Unit,
private val onBackPressed: () -> Unit,
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main), private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
) : Middleware<HistoryFragmentState, HistoryFragmentAction> { ) : Middleware<HistoryFragmentState, HistoryFragmentAction> {
override fun invoke( override fun invoke(
@ -40,6 +41,18 @@ class HistoryNavigationMiddleware(
next(action) next(action)
scope.launch { scope.launch {
when (action) { when (action) {
is HistoryFragmentAction.EnterRecentlyClosed -> {
navController.navigate(
HistoryFragmentDirections.actionGlobalRecentlyClosed(),
NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build(),
)
}
is HistoryFragmentAction.BackPressed -> {
// When editing, we override the back pressed event to update the mode.
if (currentState.mode !is HistoryFragmentState.Mode.Editing) {
onBackPressed()
}
}
is HistoryFragmentAction.HistoryItemClicked -> { is HistoryFragmentAction.HistoryItemClicked -> {
if (currentState.mode.selectedItems.isEmpty()) { if (currentState.mode.selectedItems.isEmpty()) {
when (val item = action.item) { when (val item = action.item) {

@ -7,6 +7,7 @@ package org.mozilla.fenix.library.history.state
import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.library.history.History import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryFragmentAction import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryFragmentState
@ -30,6 +31,9 @@ class HistoryTelemetryMiddleware(
val currentState = context.state val currentState = context.state
next(action) next(action)
when (action) { when (action) {
is HistoryFragmentAction.EnterRecentlyClosed -> {
Events.recentlyClosedTabsOpened.record(NoExtras())
}
is HistoryFragmentAction.HistoryItemClicked -> { is HistoryFragmentAction.HistoryItemClicked -> {
if (currentState.mode.selectedItems.isEmpty()) { if (currentState.mode.selectedItems.isEmpty()) {
when (val item = action.item) { when (val item = action.item) {

@ -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.library.history.state.bindings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChangedBy
import mozilla.components.lib.state.helpers.AbstractBinding
import org.mozilla.fenix.library.history.HistoryFragmentState
import org.mozilla.fenix.library.history.HistoryFragmentStore
/**
* A binding to map state updates to menu updates.
*/
class MenuBinding(
store: HistoryFragmentStore,
val invalidateOptionsMenu: () -> Unit,
) : AbstractBinding<HistoryFragmentState>(store) {
override suspend fun onState(flow: Flow<HistoryFragmentState>) {
flow.distinctUntilChangedBy { it.mode }
.collect { invalidateOptionsMenu() }
}
}

@ -33,7 +33,11 @@ class HistoryListItemViewHolder(
init { init {
binding.recentlyClosedNavEmpty.recentlyClosedNav.setOnClickListener { binding.recentlyClosedNavEmpty.recentlyClosedNav.setOnClickListener {
historyInteractor.onRecentlyClosedClicked() if (FeatureFlags.historyFragmentLibStateRefactor) {
store.dispatch(HistoryFragmentAction.EnterRecentlyClosed)
} else {
historyInteractor.onRecentlyClosedClicked()
}
} }
binding.historyLayout.overflowView.apply { binding.historyLayout.overflowView.apply {

@ -185,6 +185,30 @@ class HistoryFragmentStoreTest {
assertTrue(store.state.mode.selectedItems.contains(historyItem)) assertTrue(store.state.mode.selectedItems.contains(historyItem))
} }
@Test
fun `GIVEN mode is editing WHEN back pressed THEN mode becomes normal`() {
val store = HistoryFragmentStore(
emptyDefaultState().copy(
mode = HistoryFragmentState.Mode.Editing(
setOf(),
),
),
)
store.dispatch(HistoryFragmentAction.BackPressed).joinBlocking()
assertEquals(HistoryFragmentState.Mode.Normal, store.state.mode)
}
@Test
fun `GIVEN mode is not editing WHEN back pressed THEN does not change`() {
val store = HistoryFragmentStore(emptyDefaultState().copy(mode = HistoryFragmentState.Mode.Syncing))
store.dispatch(HistoryFragmentAction.BackPressed).joinBlocking()
assertEquals(HistoryFragmentState.Mode.Syncing, store.state.mode)
}
private fun emptyDefaultState(): HistoryFragmentState = HistoryFragmentState( private fun emptyDefaultState(): HistoryFragmentState = HistoryFragmentState(
items = listOf(), items = listOf(),
mode = HistoryFragmentState.Mode.Normal, mode = HistoryFragmentState.Mode.Normal,

@ -5,6 +5,7 @@
package org.mozilla.fenix.library.history.state package org.mozilla.fenix.library.history.state
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavOptions
import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import mozilla.components.support.test.any import mozilla.components.support.test.any
@ -17,8 +18,10 @@ import org.junit.Assert.assertTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.History import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryFragmentAction import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryFragmentState
import org.mozilla.fenix.library.history.HistoryFragmentStore import org.mozilla.fenix.library.history.HistoryFragmentStore
import org.mozilla.fenix.library.history.HistoryItemTimeGroup import org.mozilla.fenix.library.history.HistoryItemTimeGroup
@ -39,6 +42,7 @@ class HistoryNavigationMiddlewareTest {
openedInBrowser = true openedInBrowser = true
} }
}, },
onBackPressed = { },
scope = this, scope = this,
) )
val store = val store =
@ -62,6 +66,7 @@ class HistoryNavigationMiddlewareTest {
openedInBrowser = true openedInBrowser = true
} }
}, },
onBackPressed = { },
scope = this, scope = this,
) )
val state = HistoryFragmentState.initial.copy( val state = HistoryFragmentState.initial.copy(
@ -85,6 +90,7 @@ class HistoryNavigationMiddlewareTest {
val middleware = HistoryNavigationMiddleware( val middleware = HistoryNavigationMiddleware(
navController = navController, navController = navController,
openToBrowser = { }, openToBrowser = { },
onBackPressed = { },
scope = this, scope = this,
) )
val store = val store =
@ -98,4 +104,68 @@ class HistoryNavigationMiddlewareTest {
navOptions = any(), navOptions = any(),
) )
} }
@Test
fun `WHEN recently closed is requested to be entered THEN nav controller navigates to it`() = runTest {
val navController = mock<NavController>()
val middleware = HistoryNavigationMiddleware(
navController = navController,
openToBrowser = { },
onBackPressed = { },
scope = this,
)
val store =
HistoryFragmentStore(HistoryFragmentState.initial, middleware = listOf(middleware))
store.dispatch(HistoryFragmentAction.EnterRecentlyClosed).joinBlocking()
advanceUntilIdle()
verify(navController).navigate(
HistoryFragmentDirections.actionGlobalRecentlyClosed(),
NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build(),
)
}
@Test
fun `GIVEN mode is editing WHEN back pressed THEN no navigation happens`() = runTest {
var onBackPressed = false
val middleware = HistoryNavigationMiddleware(
navController = mock(),
openToBrowser = { },
onBackPressed = { onBackPressed = true },
scope = this,
)
val store =
HistoryFragmentStore(
HistoryFragmentState.initial.copy(
mode = HistoryFragmentState.Mode.Editing(
setOf(),
),
),
middleware = listOf(middleware),
)
store.dispatch(HistoryFragmentAction.BackPressed).joinBlocking()
advanceUntilIdle()
assertFalse(onBackPressed)
}
@Test
fun `GIVEN mode is not editing WHEN back pressed THEN onBackPressed callback invoked`() = runTest {
var onBackPressed = false
val middleware = HistoryNavigationMiddleware(
navController = mock(),
openToBrowser = { },
onBackPressed = { onBackPressed = true },
scope = this,
)
val store =
HistoryFragmentStore(HistoryFragmentState.initial, middleware = listOf(middleware))
store.dispatch(HistoryFragmentAction.BackPressed).joinBlocking()
advanceUntilIdle()
assertTrue(onBackPressed)
}
} }

@ -12,6 +12,7 @@ import org.junit.Assert.assertNull
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.library.history.History import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryFragmentAction import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryFragmentState
@ -108,4 +109,16 @@ class HistoryTelemetryMiddlewareTest {
assertNotNull(GleanHistory.removedAll.testGetValue()) assertNotNull(GleanHistory.removedAll.testGetValue())
} }
@Test
fun `GIVEN recently closed is requested to be entered THEN telemetry recorded`() {
val store = HistoryFragmentStore(
initialState = HistoryFragmentState.initial,
middleware = listOf(middleware),
)
store.dispatch(HistoryFragmentAction.EnterRecentlyClosed).joinBlocking()
assertNotNull(Events.recentlyClosedTabsOpened.testGetValue())
}
} }

@ -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.library.history.state.bindings
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.library.history.HistoryFragmentAction
import org.mozilla.fenix.library.history.HistoryFragmentState
import org.mozilla.fenix.library.history.HistoryFragmentStore
class MenuBindingTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
@Test
fun `WHEN the mode is updated THEN the menu is invalidated`() {
var menuInvalidated = false
val store = HistoryFragmentStore(HistoryFragmentState.initial.copy(mode = HistoryFragmentState.Mode.Syncing))
val binding = MenuBinding(
store = store,
invalidateOptionsMenu = { menuInvalidated = true },
)
binding.start()
store.dispatch(HistoryFragmentAction.FinishSync).joinBlocking()
assertTrue(menuInvalidated)
}
}
Loading…
Cancel
Save