For #[15083]: Add multi select to recently closed tabs
This commit is contained in:
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 item: RecoverableTab = mockk(relaxed = true)
|
val tabs = createFakeTabList(2)
|
||||||
|
|
||||||
controller.handleDeleteOne(item)
|
controller.handleOpen(tabs.toSet(), BrowsingMode.Normal)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(item))
|
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)
|
||||||
|
|
||||||
|
controller.handleDelete(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
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…
Reference in New Issue
Block a user