For #[15083]: Add multi select to recently closed tabs

upstream-sync
ZianeA 3 years ago committed by Jonathan Almeida
parent 7d5582a5bf
commit e4fa71fde7

@ -9,28 +9,42 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.selection.SelectionHolder
class RecentlyClosedAdapter( class RecentlyClosedAdapter(
private val interactor: RecentlyClosedFragmentInteractor private val interactor: RecentlyClosedFragmentInteractor
) : ListAdapter<RecoverableTab, RecentlyClosedItemViewHolder>(DiffCallback) { ) : ListAdapter<RecoverableTab, RecentlyClosedItemViewHolder>(DiffCallback),
SelectionHolder<RecoverableTab> {
private var selectedTabs: Set<RecoverableTab> = emptySet()
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
): RecentlyClosedItemViewHolder { ): RecentlyClosedItemViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(RecentlyClosedItemViewHolder.LAYOUT_ID, parent, false) .inflate(RecentlyClosedItemViewHolder.LAYOUT_ID, parent, false)
return RecentlyClosedItemViewHolder(view, interactor) return RecentlyClosedItemViewHolder(view, interactor, this)
} }
override fun onBindViewHolder(holder: RecentlyClosedItemViewHolder, position: Int) { override fun onBindViewHolder(holder: RecentlyClosedItemViewHolder, position: Int) {
holder.bind(getItem(position)) holder.bind(getItem(position))
} }
override val selectedItems: Set<RecoverableTab>
get() = selectedTabs
fun updateData(tabs: List<RecoverableTab>, selectedTabs: Set<RecoverableTab>) {
this.selectedTabs = selectedTabs
notifyItemRangeChanged(0, tabs.size)
submitList(tabs)
}
private object DiffCallback : DiffUtil.ItemCallback<RecoverableTab>() { private object DiffCallback : DiffUtil.ItemCallback<RecoverableTab>() {
override fun areItemsTheSame(oldItem: RecoverableTab, newItem: RecoverableTab) = override fun areItemsTheSame(oldItem: RecoverableTab, newItem: RecoverableTab) =
oldItem.id == newItem.id oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: RecoverableTab, newItem: RecoverableTab) = override fun areContentsTheSame(oldItem: RecoverableTab, newItem: RecoverableTab) =
oldItem.id == newItem.id oldItem == newItem
} }
} }

@ -21,18 +21,27 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
@Suppress("TooManyFunctions")
interface RecentlyClosedController { interface RecentlyClosedController {
fun handleOpen(item: RecoverableTab, mode: BrowsingMode? = null) fun handleOpen(tab: RecoverableTab, mode: BrowsingMode? = null)
fun handleDeleteOne(tab: RecoverableTab) fun handleOpen(tabs: Set<RecoverableTab>, mode: BrowsingMode? = null)
fun handleDelete(tab: RecoverableTab)
fun handleDelete(tabs: Set<RecoverableTab>)
fun handleCopyUrl(item: RecoverableTab) fun handleCopyUrl(item: RecoverableTab)
fun handleShare(item: RecoverableTab) fun handleShare(tab: RecoverableTab)
fun handleShare(tabs: Set<RecoverableTab>)
fun handleNavigateToHistory() fun handleNavigateToHistory()
fun handleRestore(item: RecoverableTab) fun handleRestore(item: RecoverableTab)
fun handleSelect(tab: RecoverableTab)
fun handleDeselect(tab: RecoverableTab)
fun handleBackPressed(): Boolean
} }
@Suppress("TooManyFunctions")
class DefaultRecentlyClosedController( class DefaultRecentlyClosedController(
private val navController: NavController, private val navController: NavController,
private val store: BrowserStore, private val browserStore: BrowserStore,
private val recentlyClosedStore: RecentlyClosedFragmentStore,
private val tabsUseCases: TabsUseCases, private val tabsUseCases: TabsUseCases,
private val resources: Resources, private val resources: Resources,
private val snackbar: FenixSnackbar, private val snackbar: FenixSnackbar,
@ -40,12 +49,30 @@ class DefaultRecentlyClosedController(
private val activity: HomeActivity, private val activity: HomeActivity,
private val openToBrowser: (item: RecoverableTab, mode: BrowsingMode?) -> Unit private val openToBrowser: (item: RecoverableTab, mode: BrowsingMode?) -> Unit
) : RecentlyClosedController { ) : RecentlyClosedController {
override fun handleOpen(item: RecoverableTab, mode: BrowsingMode?) { override fun handleOpen(tab: RecoverableTab, mode: BrowsingMode?) {
openToBrowser(item, mode) openToBrowser(tab, mode)
} }
override fun handleDeleteOne(tab: RecoverableTab) { override fun handleOpen(tabs: Set<RecoverableTab>, mode: BrowsingMode?) {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab)) recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.DeselectAll)
tabs.forEach { tab -> handleOpen(tab, mode) }
}
override fun handleSelect(tab: RecoverableTab) {
recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.Select(tab))
}
override fun handleDeselect(tab: RecoverableTab) {
recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.Deselect(tab))
}
override fun handleDelete(tab: RecoverableTab) {
browserStore.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab))
}
override fun handleDelete(tabs: Set<RecoverableTab>) {
recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.DeselectAll)
tabs.forEach { tab -> handleDelete(tab) }
} }
override fun handleNavigateToHistory() { override fun handleNavigateToHistory() {
@ -64,10 +91,13 @@ class DefaultRecentlyClosedController(
} }
} }
override fun handleShare(item: RecoverableTab) { override fun handleShare(tab: RecoverableTab) = handleShare(setOf(tab))
override fun handleShare(tabs: Set<RecoverableTab>) {
val shareData = tabs.map { ShareData(url = it.url, title = it.title) }
navController.navigateBlockingForAsyncNavGraph( navController.navigateBlockingForAsyncNavGraph(
RecentlyClosedFragmentDirections.actionGlobalShareFragment( RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title)) data = shareData.toTypedArray()
) )
) )
} }
@ -75,7 +105,7 @@ class DefaultRecentlyClosedController(
override fun handleRestore(item: RecoverableTab) { override fun handleRestore(item: RecoverableTab) {
tabsUseCases.restore(item) tabsUseCases.restore(item)
store.dispatch( browserStore.dispatch(
RecentlyClosedAction.RemoveClosedTabAction(item) RecentlyClosedAction.RemoveClosedTabAction(item)
) )
@ -83,4 +113,13 @@ class DefaultRecentlyClosedController(
from = BrowserDirection.FromRecentlyClosed from = BrowserDirection.FromRecentlyClosed
) )
} }
override fun handleBackPressed(): Boolean {
return if (recentlyClosedStore.state.selectedTabs.isNotEmpty()) {
recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.DeselectAll)
true
} else {
false
}
}
} }

@ -7,6 +7,7 @@ package org.mozilla.fenix.library.recentlyclosed
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -21,6 +22,7 @@ import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -30,17 +32,19 @@ import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.library.LibraryPageFragment
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() { class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>(), UserInteractionHandler {
private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore
private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null
protected val recentlyClosedFragmentView: RecentlyClosedFragmentView protected val recentlyClosedFragmentView: RecentlyClosedFragmentView
get() = _recentlyClosedFragmentView!! get() = _recentlyClosedFragmentView!!
private lateinit var recentlyClosedInteractor: RecentlyClosedFragmentInteractor private lateinit var recentlyClosedInteractor: RecentlyClosedFragmentInteractor
private lateinit var recentlyClosedController: RecentlyClosedController
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@ -48,15 +52,43 @@ class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() {
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library_menu, menu) if (recentlyClosedFragmentStore.state.selectedTabs.isNotEmpty()) {
inflater.inflate(R.menu.history_select_multi, menu)
menu.findItem(R.id.delete_history_multi_select)?.let { deleteItem ->
deleteItem.title = SpannableString(deleteItem.title)
.apply { setTextColor(requireContext(), R.attr.destructive) }
}
} else {
inflater.inflate(R.menu.library_menu, menu)
}
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean {
R.id.close_history -> { val selectedTabs = recentlyClosedFragmentStore.state.selectedTabs
close()
true return when (item.itemId) {
R.id.close_history -> {
close()
true
}
R.id.share_history_multi_select -> {
recentlyClosedController.handleShare(selectedTabs)
true
}
R.id.delete_history_multi_select -> {
recentlyClosedController.handleDelete(selectedTabs)
true
}
R.id.open_history_in_new_tabs_multi_select -> {
recentlyClosedController.handleOpen(selectedTabs, BrowsingMode.Normal)
true
}
R.id.open_history_in_private_tabs_multi_select -> {
recentlyClosedController.handleOpen(selectedTabs, BrowsingMode.Private)
true
}
else -> super.onOptionsItemSelected(item)
} }
else -> super.onOptionsItemSelected(item)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -73,25 +105,26 @@ class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() {
recentlyClosedFragmentStore = StoreProvider.get(this) { recentlyClosedFragmentStore = StoreProvider.get(this) {
RecentlyClosedFragmentStore( RecentlyClosedFragmentStore(
RecentlyClosedFragmentState( RecentlyClosedFragmentState(
items = listOf() items = listOf(),
selectedTabs = emptySet()
) )
) )
} }
recentlyClosedInteractor = RecentlyClosedFragmentInteractor( recentlyClosedController = DefaultRecentlyClosedController(
recentlyClosedController = DefaultRecentlyClosedController( navController = findNavController(),
navController = findNavController(), browserStore = requireComponents.core.store,
store = requireComponents.core.store, recentlyClosedStore = recentlyClosedFragmentStore,
activity = activity as HomeActivity, activity = activity as HomeActivity,
tabsUseCases = requireComponents.useCases.tabsUseCases, tabsUseCases = requireComponents.useCases.tabsUseCases,
resources = requireContext().resources, resources = requireContext().resources,
snackbar = FenixSnackbar.make( snackbar = FenixSnackbar.make(
view = requireActivity().getRootView()!!, view = requireActivity().getRootView()!!,
isDisplayedWithBrowserToolbar = true isDisplayedWithBrowserToolbar = true
), ),
clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager, clipboardManager = activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager,
openToBrowser = ::openItem openToBrowser = ::openItem
)
) )
recentlyClosedInteractor = RecentlyClosedFragmentInteractor(recentlyClosedController)
_recentlyClosedFragmentView = RecentlyClosedFragmentView( _recentlyClosedFragmentView = RecentlyClosedFragmentView(
view.recentlyClosedLayout, view.recentlyClosedLayout,
recentlyClosedInteractor recentlyClosedInteractor
@ -116,8 +149,9 @@ class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() {
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(recentlyClosedFragmentStore) { consumeFrom(recentlyClosedFragmentStore) { state ->
recentlyClosedFragmentView.update(it.items) recentlyClosedFragmentView.update(state)
activity?.invalidateOptionsMenu()
} }
requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow -> requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow ->
@ -132,4 +166,8 @@ class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() {
} }
override val selectedItems: Set<RecoverableTab> = setOf() override val selectedItems: Set<RecoverableTab> = setOf()
override fun onBackPressed(): Boolean {
return recentlyClosedController.handleBackPressed()
}
} }

@ -34,11 +34,23 @@ class RecentlyClosedFragmentInteractor(
recentlyClosedController.handleOpen(item, BrowsingMode.Private) recentlyClosedController.handleOpen(item, BrowsingMode.Private)
} }
override fun onDeleteOne(tab: RecoverableTab) { override fun onDelete(tab: RecoverableTab) {
recentlyClosedController.handleDeleteOne(tab) recentlyClosedController.handleDelete(tab)
} }
override fun onNavigateToHistory() { override fun onNavigateToHistory() {
recentlyClosedController.handleNavigateToHistory() recentlyClosedController.handleNavigateToHistory()
} }
override fun open(item: RecoverableTab) {
recentlyClosedController.handleRestore(item)
}
override fun select(item: RecoverableTab) {
recentlyClosedController.handleSelect(item)
}
override fun deselect(item: RecoverableTab) {
recentlyClosedController.handleDeselect(item)
}
} }

@ -24,13 +24,19 @@ class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) :
*/ */
sealed class RecentlyClosedFragmentAction : Action { sealed class RecentlyClosedFragmentAction : Action {
data class Change(val list: List<RecoverableTab>) : RecentlyClosedFragmentAction() data class Change(val list: List<RecoverableTab>) : RecentlyClosedFragmentAction()
data class Select(val tab: RecoverableTab) : RecentlyClosedFragmentAction()
data class Deselect(val tab: RecoverableTab) : RecentlyClosedFragmentAction()
object DeselectAll : RecentlyClosedFragmentAction()
} }
/** /**
* The state for the Recently Closed Screen * The state for the Recently Closed Screen
* @property items List of recently closed tabs to display * @property items List of recently closed tabs to display
*/ */
data class RecentlyClosedFragmentState(val items: List<RecoverableTab> = emptyList()) : State data class RecentlyClosedFragmentState(
val items: List<RecoverableTab> = emptyList(),
val selectedTabs: Set<RecoverableTab>
) : State
/** /**
* The RecentlyClosedFragmentState Reducer. * The RecentlyClosedFragmentState Reducer.
@ -41,5 +47,12 @@ private fun recentlyClosedStateReducer(
): RecentlyClosedFragmentState { ): RecentlyClosedFragmentState {
return when (action) { return when (action) {
is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list) is RecentlyClosedFragmentAction.Change -> state.copy(items = action.list)
is RecentlyClosedFragmentAction.Select -> {
state.copy(selectedTabs = state.selectedTabs + action.tab)
}
is RecentlyClosedFragmentAction.Deselect -> {
state.copy(selectedTabs = state.selectedTabs - action.tab)
}
RecentlyClosedFragmentAction.DeselectAll -> state.copy(selectedTabs = emptySet())
} }
} }

@ -5,17 +5,19 @@
package org.mozilla.fenix.library.recentlyclosed package org.mozilla.fenix.library.recentlyclosed
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer import androidx.recyclerview.widget.SimpleItemAnimator
import kotlinx.android.synthetic.main.component_recently_closed.* import kotlinx.android.synthetic.main.component_recently_closed.*
import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.selection.SelectionInteractor
interface RecentlyClosedInteractor { interface RecentlyClosedInteractor : SelectionInteractor<RecoverableTab> {
/** /**
* Called when an item is tapped to restore it. * Called when an item is tapped to restore it.
* *
@ -57,11 +59,11 @@ interface RecentlyClosedInteractor {
fun onOpenInPrivateTab(item: RecoverableTab) fun onOpenInPrivateTab(item: RecoverableTab)
/** /**
* Deletes one recently closed tab item. * Called when recently closed tab is selected for deletion.
* *
* @param tab the recently closed tab item to delete. * @param tab the recently closed tab to delete.
*/ */
fun onDeleteOne(tab: RecoverableTab) fun onDelete(tab: RecoverableTab)
} }
/** /**
@ -70,11 +72,10 @@ interface RecentlyClosedInteractor {
class RecentlyClosedFragmentView( class RecentlyClosedFragmentView(
container: ViewGroup, container: ViewGroup,
private val interactor: RecentlyClosedFragmentInteractor private val interactor: RecentlyClosedFragmentInteractor
) : LayoutContainer { ) : LibraryPageView(container) {
override val containerView: ConstraintLayout = LayoutInflater.from(container.context) val view: View = LayoutInflater.from(container.context)
.inflate(R.layout.component_recently_closed, container, true) .inflate(R.layout.component_recently_closed, container, true)
.findViewById(R.id.recently_closed_wrapper)
private val recentlyClosedAdapter: RecentlyClosedAdapter = RecentlyClosedAdapter(interactor) private val recentlyClosedAdapter: RecentlyClosedAdapter = RecentlyClosedAdapter(interactor)
@ -82,6 +83,7 @@ class RecentlyClosedFragmentView(
recently_closed_list.apply { recently_closed_list.apply {
layoutManager = LinearLayoutManager(containerView.context) layoutManager = LinearLayoutManager(containerView.context)
adapter = recentlyClosedAdapter adapter = recentlyClosedAdapter
(itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
} }
view_more_history.apply { view_more_history.apply {
@ -102,9 +104,20 @@ class RecentlyClosedFragmentView(
} }
} }
fun update(items: List<RecoverableTab>) { fun update(state: RecentlyClosedFragmentState) {
recently_closed_empty_view.isVisible = items.isEmpty() state.apply {
recently_closed_list.isVisible = items.isNotEmpty() recently_closed_empty_view.isVisible = items.isEmpty()
recentlyClosedAdapter.submitList(items) recently_closed_list.isVisible = items.isNotEmpty()
recentlyClosedAdapter.updateData(items, selectedTabs)
if (selectedTabs.isEmpty()) {
setUiForNormalMode(context.getString(R.string.library_recently_closed_tabs))
} else {
setUiForSelectingMode(
context.getString(R.string.history_multi_select_title, selectedTabs.size)
)
}
}
} }
} }

@ -7,14 +7,19 @@ package org.mozilla.fenix.library.recentlyclosed
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.* import kotlinx.android.synthetic.main.history_list_item.view.*
import kotlinx.android.synthetic.main.library_site_item.view.*
import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.library.history.HistoryItemMenu import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.utils.Do import org.mozilla.fenix.utils.Do
class RecentlyClosedItemViewHolder( class RecentlyClosedItemViewHolder(
view: View, view: View,
private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor,
private val selectionHolder: SelectionHolder<RecoverableTab>
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private var item: RecoverableTab? = null private var item: RecoverableTab? = null
@ -30,12 +35,17 @@ class RecentlyClosedItemViewHolder(
if (item.title.isNotEmpty()) item.title else item.url if (item.title.isNotEmpty()) item.title else item.url
itemView.history_layout.urlView.text = item.url itemView.history_layout.urlView.text = item.url
itemView.history_layout.setSelectionInteractor(item, selectionHolder, recentlyClosedFragmentInteractor)
itemView.history_layout.changeSelected(item in selectionHolder.selectedItems)
if (this.item?.url != item.url) { if (this.item?.url != item.url) {
itemView.history_layout.loadFavicon(item.url) itemView.history_layout.loadFavicon(item.url)
} }
itemView.setOnClickListener { if (selectionHolder.selectedItems.isEmpty()) {
recentlyClosedFragmentInteractor.restore(item) itemView.overflow_menu.showAndEnable()
} else {
itemView.overflow_menu.hideAndDisable()
} }
this.item = item this.item = item
@ -53,9 +63,7 @@ class RecentlyClosedItemViewHolder(
HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab( HistoryItemMenu.Item.OpenInPrivateTab -> recentlyClosedFragmentInteractor.onOpenInPrivateTab(
item item
) )
HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDeleteOne( HistoryItemMenu.Item.Delete -> recentlyClosedFragmentInteractor.onDelete(item)
item
)
} }
} }

@ -18,7 +18,8 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recently_closed_list" android:id="@+id/recently_closed_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_more_history" app:layout_constraintTop_toBottomOf="@id/view_more_history"
tools:listitem="@layout/history_list_item" /> tools:listitem="@layout/history_list_item" />

@ -32,6 +32,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.ext.directionsEq import org.mozilla.fenix.ext.directionsEq
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.optionsEq import org.mozilla.fenix.ext.optionsEq
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@ -46,13 +47,15 @@ class DefaultRecentlyClosedControllerTest {
private val clipboardManager: ClipboardManager = mockk(relaxed = true) private val clipboardManager: ClipboardManager = mockk(relaxed = true)
private val openToBrowser: (RecoverableTab, BrowsingMode?) -> Unit = mockk(relaxed = true) private val openToBrowser: (RecoverableTab, BrowsingMode?) -> Unit = mockk(relaxed = true)
private val activity: HomeActivity = mockk(relaxed = true) private val activity: HomeActivity = mockk(relaxed = true)
private val store: BrowserStore = mockk(relaxed = true) private val browserStore: BrowserStore = mockk(relaxed = true)
private val recentlyClosedStore: RecentlyClosedFragmentStore = mockk(relaxed = true)
private val tabsUseCases: TabsUseCases = mockk(relaxed = true) private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
val mockedTab: RecoverableTab = mockk(relaxed = true) val mockedTab: RecoverableTab = mockk(relaxed = true)
private val controller = DefaultRecentlyClosedController( private val controller = DefaultRecentlyClosedController(
navController, navController,
store, browserStore,
recentlyClosedStore,
tabsUseCases, tabsUseCases,
resources, resources,
snackbar, snackbar,
@ -89,13 +92,62 @@ class DefaultRecentlyClosedControllerTest {
} }
@Test @Test
fun handleDeleteOne() { fun `open multiple tabs`() {
val tabs = createFakeTabList(2)
controller.handleOpen(tabs.toSet(), BrowsingMode.Normal)
verify {
openToBrowser(tabs[0], BrowsingMode.Normal)
openToBrowser(tabs[1], BrowsingMode.Normal)
}
controller.handleOpen(tabs.toSet(), BrowsingMode.Private)
verify {
openToBrowser(tabs[0], BrowsingMode.Private)
openToBrowser(tabs[1], BrowsingMode.Private)
}
}
@Test
fun `handle select tab`() {
val selectedTab = createFakeTab()
controller.handleSelect(selectedTab)
verify { recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.Select(selectedTab)) }
}
@Test
fun `handle deselect tab`() {
val deselectedTab = createFakeTab()
controller.handleDeselect(deselectedTab)
verify { recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.Deselect(deselectedTab)) }
}
@Test
fun handleDelete() {
val item: RecoverableTab = mockk(relaxed = true) val item: RecoverableTab = mockk(relaxed = true)
controller.handleDeleteOne(item) controller.handleDelete(item)
verify { verify {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item)) browserStore.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item))
}
}
@Test
fun `delete multiple tabs`() {
val tabs = createFakeTabList(2)
controller.handleDelete(tabs.toSet())
verify {
browserStore.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tabs[0]))
browserStore.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tabs[1]))
} }
} }
@ -104,7 +156,7 @@ class DefaultRecentlyClosedControllerTest {
controller.handleNavigateToHistory() controller.handleNavigateToHistory()
verify { verify {
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
directionsEq( directionsEq(
RecentlyClosedFragmentDirections.actionGlobalHistoryFragment() RecentlyClosedFragmentDirections.actionGlobalHistoryFragment()
), ),
@ -139,7 +191,7 @@ class DefaultRecentlyClosedControllerTest {
controller.handleShare(item) controller.handleShare(item)
verify { verify {
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
directionsEq( directionsEq(
RecentlyClosedFragmentDirections.actionGlobalShareFragment( RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title)) data = arrayOf(ShareData(url = item.url, title = item.title))
@ -149,6 +201,23 @@ class DefaultRecentlyClosedControllerTest {
} }
} }
@Test
fun `share multiple tabs`() {
val tabs = createFakeTabList(2)
controller.handleShare(tabs.toSet())
verify {
val data = arrayOf(
ShareData(title = tabs[0].title, url = tabs[0].url),
ShareData(title = tabs[1].title, url = tabs[1].url)
)
navController.navigateBlockingForAsyncNavGraph(
directionsEq(RecentlyClosedFragmentDirections.actionGlobalShareFragment(data))
)
}
}
@Test @Test
fun handleRestore() { fun handleRestore() {
controller.handleRestore(mockedTab) controller.handleRestore(mockedTab)
@ -157,4 +226,25 @@ class DefaultRecentlyClosedControllerTest {
verify { tabsUseCases.restore.invoke(mockedTab, true) } verify { tabsUseCases.restore.invoke(mockedTab, true) }
} }
@Test
fun `exist multi-select mode when back is pressed`() {
every { recentlyClosedStore.state.selectedTabs } returns createFakeTabList(3).toSet()
controller.handleBackPressed()
verify { recentlyClosedStore.dispatch(RecentlyClosedFragmentAction.DeselectAll) }
}
private fun createFakeTab(id: String = "FakeId", url: String = "www.fake.com"): RecoverableTab =
RecoverableTab(id, url)
private fun createFakeTabList(size: Int): List<RecoverableTab> {
val fakeTabs = mutableListOf<RecoverableTab>()
for (i in 0 until size) {
fakeTabs.add(createFakeTab(id = "FakeId$i"))
}
return fakeTabs
}
} }

@ -76,12 +76,12 @@ class RecentlyClosedFragmentInteractorTest {
} }
@Test @Test
fun onDeleteOne() { fun onDelete() {
val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L) val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.onDeleteOne(tab) interactor.onDelete(tab)
verify { verify {
defaultRecentlyClosedController.handleDeleteOne(tab) defaultRecentlyClosedController.handleDelete(tab)
} }
} }

Loading…
Cancel
Save