For #20893 - Search term groups in history
parent
391ff6b5fd
commit
2ae7d5d593
@ -1,68 +0,0 @@
|
||||
/* 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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.browser.menu2.BrowserMenuController
|
||||
import mozilla.components.concept.menu.MenuController
|
||||
import mozilla.components.concept.menu.candidate.TextMenuCandidate
|
||||
import mozilla.components.concept.menu.candidate.TextStyle
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class HistoryItemMenu(
|
||||
private val context: Context,
|
||||
private val onItemTapped: (Item) -> Unit
|
||||
) {
|
||||
|
||||
enum class Item {
|
||||
Copy,
|
||||
Share,
|
||||
OpenInNewTab,
|
||||
OpenInPrivateTab,
|
||||
Delete;
|
||||
}
|
||||
|
||||
val menuController: MenuController by lazy {
|
||||
BrowserMenuController().apply {
|
||||
submitList(menuItems())
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun menuItems(): List<TextMenuCandidate> {
|
||||
return listOf(
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.history_menu_copy_button)
|
||||
) {
|
||||
onItemTapped.invoke(Item.Copy)
|
||||
},
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.history_menu_share_button)
|
||||
) {
|
||||
onItemTapped.invoke(Item.Share)
|
||||
},
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.history_menu_open_in_new_tab_button)
|
||||
) {
|
||||
onItemTapped.invoke(Item.OpenInNewTab)
|
||||
},
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.history_menu_open_in_private_tab_button)
|
||||
) {
|
||||
onItemTapped.invoke(Item.OpenInPrivateTab)
|
||||
},
|
||||
TextMenuCandidate(
|
||||
text = context.getString(R.string.history_delete_item),
|
||||
textStyle = TextStyle(
|
||||
color = context.getColorFromAttr(R.attr.destructive)
|
||||
)
|
||||
) {
|
||||
onItemTapped.invoke(Item.Delete)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.databinding.FragmentHistoryMetadataGroupBinding
|
||||
import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.ext.setTextColor
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.controller.DefaultHistoryMetadataGroupController
|
||||
import org.mozilla.fenix.library.historymetadata.interactor.DefaultHistoryMetadataGroupInteractor
|
||||
import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor
|
||||
import org.mozilla.fenix.library.historymetadata.view.HistoryMetadataGroupView
|
||||
|
||||
/**
|
||||
* Displays a list of history metadata items for a history metadata search group.
|
||||
*/
|
||||
class HistoryMetadataGroupFragment : LibraryPageFragment<History.Metadata>(), UserInteractionHandler {
|
||||
|
||||
private lateinit var historyMetadataGroupStore: HistoryMetadataGroupFragmentStore
|
||||
private lateinit var interactor: HistoryMetadataGroupInteractor
|
||||
|
||||
private var _historyMetadataGroupView: HistoryMetadataGroupView? = null
|
||||
private val historyMetadataGroupView: HistoryMetadataGroupView
|
||||
get() = _historyMetadataGroupView!!
|
||||
|
||||
private val args by navArgs<HistoryMetadataGroupFragmentArgs>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val binding = FragmentHistoryMetadataGroupBinding.inflate(inflater, container, false)
|
||||
|
||||
historyMetadataGroupStore = StoreProvider.get(this) {
|
||||
HistoryMetadataGroupFragmentStore(
|
||||
HistoryMetadataGroupFragmentState(
|
||||
items = args.historyMetadataItems.filterIsInstance<History.Metadata>()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
interactor = DefaultHistoryMetadataGroupInteractor(
|
||||
controller = DefaultHistoryMetadataGroupController(
|
||||
activity = activity as HomeActivity,
|
||||
store = historyMetadataGroupStore,
|
||||
navController = findNavController()
|
||||
)
|
||||
)
|
||||
|
||||
_historyMetadataGroupView = HistoryMetadataGroupView(
|
||||
container = binding.historyMetadataGroupLayout,
|
||||
interactor = interactor,
|
||||
title = args.title
|
||||
)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
consumeFrom(historyMetadataGroupStore) { state ->
|
||||
historyMetadataGroupView.update(state)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showToolbar(args.title)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (selectedItems.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.history_menu, menu)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.share_history_multi_select -> {
|
||||
interactor.onShareMenuItem(selectedItems)
|
||||
true
|
||||
}
|
||||
R.id.delete_history_multi_select -> {
|
||||
interactor.onDeleteMenuItem(selectedItems)
|
||||
true
|
||||
}
|
||||
R.id.open_history_in_new_tabs_multi_select -> {
|
||||
openItemsInNewTab { selectedItem ->
|
||||
selectedItem.url
|
||||
}
|
||||
|
||||
showTabTray()
|
||||
true
|
||||
}
|
||||
R.id.open_history_in_private_tabs_multi_select -> {
|
||||
openItemsInNewTab(private = true) { selectedItem ->
|
||||
selectedItem.url
|
||||
}
|
||||
|
||||
(activity as HomeActivity).apply {
|
||||
browsingModeManager.mode = BrowsingMode.Private
|
||||
supportActionBar?.hide()
|
||||
}
|
||||
|
||||
showTabTray()
|
||||
true
|
||||
}
|
||||
R.id.history_delete_all -> {
|
||||
interactor.onDeleteAllMenuItem()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_historyMetadataGroupView = null
|
||||
}
|
||||
|
||||
override val selectedItems: Set<History.Metadata> get() =
|
||||
historyMetadataGroupStore.state.items.filter { it.selected }.toSet()
|
||||
|
||||
override fun onBackPressed(): Boolean = interactor.onBackPressed(selectedItems)
|
||||
|
||||
private fun showTabTray() {
|
||||
findNavController().nav(
|
||||
R.id.historyMetadataGroupFragment,
|
||||
HistoryMetadataGroupFragmentDirections.actionGlobalTabsTrayFragment()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.fenix.library.history.History
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [HistoryMetadataGroupFragmentState] and applying
|
||||
* [HistoryMetadataGroupFragmentAction]s.
|
||||
*/
|
||||
class HistoryMetadataGroupFragmentStore(initialState: HistoryMetadataGroupFragmentState) :
|
||||
Store<HistoryMetadataGroupFragmentState, HistoryMetadataGroupFragmentAction>(
|
||||
initialState,
|
||||
::historyStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the [HistoryMetadataGroupFragmentStore to modify the
|
||||
* [HistoryMetadataGroupFragmentState] through the [historyStateReducer].
|
||||
*/
|
||||
sealed class HistoryMetadataGroupFragmentAction : Action {
|
||||
data class UpdateHistoryItems(val items: List<History.Metadata>) :
|
||||
HistoryMetadataGroupFragmentAction()
|
||||
data class Select(val item: History.Metadata) : HistoryMetadataGroupFragmentAction()
|
||||
data class Deselect(val item: History.Metadata) : HistoryMetadataGroupFragmentAction()
|
||||
object DeselectAll : HistoryMetadataGroupFragmentAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* The state for [HistoryMetadataGroupFragment].
|
||||
*
|
||||
* @property items The list of [History.Metadata] to display.
|
||||
*/
|
||||
data class HistoryMetadataGroupFragmentState(
|
||||
val items: List<History.Metadata> = emptyList()
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Reduces the history metadata state from the current state with the provided [action] to be
|
||||
* performed.
|
||||
*
|
||||
* @param state The current history metadata state.
|
||||
* @param action The action to be performed on the state.
|
||||
* @return the new [HistoryMetadataGroupFragmentState] with the [action] executed.
|
||||
*/
|
||||
private fun historyStateReducer(
|
||||
state: HistoryMetadataGroupFragmentState,
|
||||
action: HistoryMetadataGroupFragmentAction
|
||||
): HistoryMetadataGroupFragmentState {
|
||||
return when (action) {
|
||||
is HistoryMetadataGroupFragmentAction.UpdateHistoryItems ->
|
||||
state.copy(items = action.items)
|
||||
is HistoryMetadataGroupFragmentAction.Select ->
|
||||
state.copy(
|
||||
items = state.items.toMutableList()
|
||||
.map {
|
||||
if (it == action.item) {
|
||||
it.copy(selected = true)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
)
|
||||
is HistoryMetadataGroupFragmentAction.Deselect ->
|
||||
state.copy(
|
||||
items = state.items.toMutableList()
|
||||
.map {
|
||||
if (it == action.item) {
|
||||
it.copy(selected = false)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
)
|
||||
is HistoryMetadataGroupFragmentAction.DeselectAll ->
|
||||
state.copy(
|
||||
items = state.items.toMutableList()
|
||||
.map { it.copy(selected = false) }
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/* 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.historymetadata.controller
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentAction
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore
|
||||
|
||||
/**
|
||||
* An interface that handles the view manipulation of the history metadata group in the History
|
||||
* metadata group screen.
|
||||
*/
|
||||
interface HistoryMetadataGroupController {
|
||||
|
||||
/**
|
||||
* Opens the given history [item] in a new tab.
|
||||
*
|
||||
* @param item The [History] to open in a new tab.
|
||||
*/
|
||||
fun handleOpen(item: History.Metadata)
|
||||
|
||||
/**
|
||||
* Toggles the given history [item] to be selected in multi-select mode.
|
||||
*
|
||||
* @param item The [History] to select.
|
||||
*/
|
||||
fun handleSelect(item: History.Metadata)
|
||||
|
||||
/**
|
||||
* Toggles the given history [item] to be deselected in multi-select mode.
|
||||
*
|
||||
* @param item The [History] to deselect.
|
||||
*/
|
||||
fun handleDeselect(item: History.Metadata)
|
||||
|
||||
/**
|
||||
* Called on backpressed to deselect all the given [items].
|
||||
*
|
||||
* @param items The set of [History]s to deselect.
|
||||
*/
|
||||
fun handleBackPressed(items: Set<History.Metadata>): Boolean
|
||||
|
||||
/**
|
||||
* Opens the share sheet for a set of history [items].
|
||||
*
|
||||
* @param items The set of [History]s to share.
|
||||
*/
|
||||
fun handleShare(items: Set<History.Metadata>)
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of [HistoryMetadataGroupController].
|
||||
*/
|
||||
class DefaultHistoryMetadataGroupController(
|
||||
private val activity: HomeActivity,
|
||||
private val store: HistoryMetadataGroupFragmentStore,
|
||||
private val navController: NavController,
|
||||
) : HistoryMetadataGroupController {
|
||||
|
||||
override fun handleOpen(item: History.Metadata) {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = item.url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHistoryMetadataGroup
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleSelect(item: History.Metadata) {
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Select(item))
|
||||
}
|
||||
|
||||
override fun handleDeselect(item: History.Metadata) {
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Deselect(item))
|
||||
}
|
||||
|
||||
override fun handleBackPressed(items: Set<History.Metadata>): Boolean {
|
||||
return if (items.isNotEmpty()) {
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.DeselectAll)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleShare(items: Set<History.Metadata>) {
|
||||
navController.navigate(
|
||||
HistoryMetadataGroupFragmentDirections.actionGlobalShareFragment(
|
||||
data = items.map { ShareData(url = it.url, title = it.title) }.toTypedArray()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/* 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.historymetadata.interactor
|
||||
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.controller.HistoryMetadataGroupController
|
||||
import org.mozilla.fenix.selection.SelectionInteractor
|
||||
|
||||
/**
|
||||
* Interface for history metadata group related actions in the History view.
|
||||
*/
|
||||
interface HistoryMetadataGroupInteractor : SelectionInteractor<History.Metadata> {
|
||||
|
||||
/**
|
||||
* Called on backpressed to deselect all the given [items].
|
||||
*
|
||||
* @param items The set of [History]s to deselect.
|
||||
*/
|
||||
fun onBackPressed(items: Set<History.Metadata>): Boolean
|
||||
|
||||
/**
|
||||
* Deletes the given set of history [items] that are selected. Called when a user clicks on the
|
||||
* "Delete" menu item.
|
||||
*
|
||||
* @param items The set of [History]s to delete.
|
||||
*/
|
||||
fun onDeleteMenuItem(items: Set<History.Metadata>)
|
||||
|
||||
/**
|
||||
* Deletes the all the history items in the history metadata group. Called when a user clicks
|
||||
* on the "Delete history" menu item.
|
||||
*/
|
||||
fun onDeleteAllMenuItem()
|
||||
|
||||
/**
|
||||
* Opens the share sheet for a set of history [items]. Called when a user clicks on the
|
||||
* "Share" menu item.
|
||||
*
|
||||
* @param items The set of [History]s to share.
|
||||
*/
|
||||
fun onShareMenuItem(items: Set<History.Metadata>)
|
||||
}
|
||||
|
||||
/**
|
||||
* The default implementation of [HistoryMetadataGroupInteractor].
|
||||
*/
|
||||
class DefaultHistoryMetadataGroupInteractor(
|
||||
private val controller: HistoryMetadataGroupController
|
||||
) : HistoryMetadataGroupInteractor {
|
||||
|
||||
override fun open(item: History.Metadata) {
|
||||
controller.handleOpen(item)
|
||||
}
|
||||
|
||||
override fun select(item: History.Metadata) {
|
||||
controller.handleSelect(item)
|
||||
}
|
||||
|
||||
override fun deselect(item: History.Metadata) {
|
||||
controller.handleDeselect(item)
|
||||
}
|
||||
|
||||
override fun onBackPressed(items: Set<History.Metadata>): Boolean {
|
||||
return controller.handleBackPressed(items)
|
||||
}
|
||||
|
||||
override fun onDeleteMenuItem(items: Set<History.Metadata>) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun onDeleteAllMenuItem() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
override fun onShareMenuItem(items: Set<History.Metadata>) {
|
||||
controller.handleShare(items)
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/* 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.historymetadata.view
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
|
||||
/**
|
||||
* Adapter for a list of history metadata items to be displayed.
|
||||
*/
|
||||
class HistoryMetadataGroupAdapter(
|
||||
private val interactor: HistoryMetadataGroupInteractor
|
||||
) : ListAdapter<History.Metadata, HistoryMetadataGroupItemViewHolder>(DiffCallback),
|
||||
SelectionHolder<History.Metadata> {
|
||||
|
||||
private var selectedHistoryItems: Set<History.Metadata> = emptySet()
|
||||
|
||||
override val selectedItems: Set<History.Metadata>
|
||||
get() = selectedHistoryItems
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): HistoryMetadataGroupItemViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(HistoryMetadataGroupItemViewHolder.LAYOUT_ID, parent, false)
|
||||
return HistoryMetadataGroupItemViewHolder(view, interactor, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HistoryMetadataGroupItemViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
fun updateData(items: List<History.Metadata>) {
|
||||
this.selectedHistoryItems = items.filter { it.selected }.toSet()
|
||||
notifyItemRangeChanged(0, items.size)
|
||||
submitList(items)
|
||||
}
|
||||
|
||||
internal object DiffCallback : DiffUtil.ItemCallback<History.Metadata>() {
|
||||
override fun areContentsTheSame(oldItem: History.Metadata, newItem: History.Metadata): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
|
||||
override fun areItemsTheSame(oldItem: History.Metadata, newItem: History.Metadata): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/* 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.historymetadata.view
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.HistoryMetadataGroupListItemBinding
|
||||
import org.mozilla.fenix.ext.hideAndDisable
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
|
||||
/**
|
||||
* View holder for a history metadata list item.
|
||||
*/
|
||||
class HistoryMetadataGroupItemViewHolder(
|
||||
view: View,
|
||||
private val interactor: HistoryMetadataGroupInteractor,
|
||||
private val selectionHolder: SelectionHolder<History.Metadata>
|
||||
) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val binding = HistoryMetadataGroupListItemBinding.bind(view)
|
||||
|
||||
private var item: History.Metadata? = null
|
||||
|
||||
fun bind(item: History.Metadata) {
|
||||
binding.historyLayout.titleView.text = item.title
|
||||
binding.historyLayout.urlView.text = item.url
|
||||
|
||||
binding.historyLayout.setSelectionInteractor(item, selectionHolder, interactor)
|
||||
binding.historyLayout.changeSelected(item in selectionHolder.selectedItems)
|
||||
|
||||
if (this.item?.url != item.url) {
|
||||
binding.historyLayout.loadFavicon(item.url)
|
||||
}
|
||||
|
||||
binding.historyLayout.overflowView.setImageResource(R.drawable.ic_close)
|
||||
|
||||
if (selectionHolder.selectedItems.isEmpty()) {
|
||||
binding.historyLayout.overflowView.showAndEnable()
|
||||
} else {
|
||||
binding.historyLayout.overflowView.hideAndDisable()
|
||||
}
|
||||
|
||||
this.item = item
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LAYOUT_ID = R.layout.history_metadata_group_list_item
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/* 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.historymetadata.view
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.databinding.ComponentHistoryMetadataGroupBinding
|
||||
import org.mozilla.fenix.library.LibraryPageView
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentState
|
||||
import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor
|
||||
|
||||
/**
|
||||
* Shows a list of history metadata items.
|
||||
*/
|
||||
class HistoryMetadataGroupView(
|
||||
container: ViewGroup,
|
||||
val interactor: HistoryMetadataGroupInteractor,
|
||||
val title: String
|
||||
) : LibraryPageView(container) {
|
||||
|
||||
private val binding = ComponentHistoryMetadataGroupBinding.inflate(
|
||||
LayoutInflater.from(container.context), container, true
|
||||
)
|
||||
|
||||
private val historyMetadataGroupAdapter = HistoryMetadataGroupAdapter(interactor)
|
||||
|
||||
init {
|
||||
binding.historyMetadataGroupList.apply {
|
||||
layoutManager = LinearLayoutManager(containerView.context)
|
||||
adapter = historyMetadataGroupAdapter
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the display of the history metadata items based on the given
|
||||
* [HistoryMetadataGroupFragmentState].
|
||||
*/
|
||||
fun update(state: HistoryMetadataGroupFragmentState) {
|
||||
binding.historyMetadataGroupList.isVisible = state.items.isNotEmpty()
|
||||
binding.historyMetadataGroupEmptyView.isVisible = state.items.isEmpty()
|
||||
|
||||
historyMetadataGroupAdapter.updateData(state.items)
|
||||
|
||||
val selectedItems = state.items.filter { it.selected }
|
||||
|
||||
if (selectedItems.isEmpty()) {
|
||||
setUiForNormalMode(title)
|
||||
} else {
|
||||
setUiForSelectingMode(
|
||||
context.getString(R.string.history_multi_select_title, selectedItems.size)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/history_metadata_group_empty_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/history_empty_message"
|
||||
android:textColor="?secondaryText"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/history_metadata_group_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:listitem="@layout/history_list_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/historyMetadataGroupLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical" />
|
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:importantForAccessibility="no"
|
||||
android:orientation="vertical">
|
||||
|
||||
<org.mozilla.fenix.library.LibrarySiteItemView
|
||||
android:id="@+id/history_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="@dimen/library_item_height" />
|
||||
</LinearLayout>
|
@ -1,76 +0,0 @@
|
||||
/* 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
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import mozilla.components.concept.menu.candidate.TextStyle
|
||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.library.history.HistoryItemMenu.Item
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class HistoryItemMenuTest {
|
||||
|
||||
private lateinit var context: Context
|
||||
private lateinit var menu: HistoryItemMenu
|
||||
private var onItemTappedCaptured: Item? = null
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
|
||||
onItemTappedCaptured = null
|
||||
menu = HistoryItemMenu(context) {
|
||||
onItemTappedCaptured = it
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete item has special styling`() {
|
||||
val deleteItem = menu.menuItems().last()
|
||||
assertEquals("Delete", deleteItem.text)
|
||||
assertEquals(
|
||||
TextStyle(color = context.getColorFromAttr(R.attr.destructive)),
|
||||
deleteItem.textStyle
|
||||
)
|
||||
|
||||
deleteItem.onClick()
|
||||
assertEquals(Item.Delete, onItemTappedCaptured)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `builds menu items`() {
|
||||
val items = menu.menuItems()
|
||||
assertEquals(5, items.size)
|
||||
val (copy, share, openInNewTab, openInPrivateTab, delete) = items
|
||||
|
||||
assertEquals("Copy", copy.text)
|
||||
assertEquals("Share", share.text)
|
||||
assertEquals("Open in new tab", openInNewTab.text)
|
||||
assertEquals("Open in private tab", openInPrivateTab.text)
|
||||
assertEquals("Delete", delete.text)
|
||||
|
||||
copy.onClick()
|
||||
assertEquals(Item.Copy, onItemTappedCaptured)
|
||||
|
||||
share.onClick()
|
||||
assertEquals(Item.Share, onItemTappedCaptured)
|
||||
|
||||
openInNewTab.onClick()
|
||||
assertEquals(Item.OpenInNewTab, onItemTappedCaptured)
|
||||
|
||||
openInPrivateTab.onClick()
|
||||
assertEquals(Item.OpenInPrivateTab, onItemTappedCaptured)
|
||||
|
||||
delete.onClick()
|
||||
assertEquals(Item.Delete, onItemTappedCaptured)
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/* 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.historymetadata
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.library.history.History
|
||||
|
||||
class HistoryMetadataGroupFragmentStoreTest {
|
||||
|
||||
private lateinit var state: HistoryMetadataGroupFragmentState
|
||||
private lateinit var store: HistoryMetadataGroupFragmentStore
|
||||
|
||||
private val mozillaHistoryMetadataItem = History.Metadata(
|
||||
id = 0,
|
||||
title = "Mozilla",
|
||||
url = "mozilla.org",
|
||||
visitedAt = 0,
|
||||
totalViewTime = 0
|
||||
)
|
||||
private val firefoxHistoryMetadataItem = History.Metadata(
|
||||
id = 0,
|
||||
title = "Firefox",
|
||||
url = "firefox.com",
|
||||
visitedAt = 0,
|
||||
totalViewTime = 0
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
state = HistoryMetadataGroupFragmentState()
|
||||
store = HistoryMetadataGroupFragmentStore(state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test updating the items in HistoryMetadataGroupFragmentStore`() = runBlocking {
|
||||
assertEquals(0, store.state.items.size)
|
||||
|
||||
val items = listOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem)
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.UpdateHistoryItems(items)).join()
|
||||
|
||||
assertEquals(items, store.state.items)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test selecting and deselecting an item in HistoryMetadataGroupFragmentStore`() = runBlocking {
|
||||
val items = listOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem)
|
||||
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.UpdateHistoryItems(items)).join()
|
||||
|
||||
assertFalse(store.state.items[0].selected)
|
||||
assertFalse(store.state.items[1].selected)
|
||||
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Select(mozillaHistoryMetadataItem)).join()
|
||||
|
||||
assertTrue(store.state.items[0].selected)
|
||||
assertFalse(store.state.items[1].selected)
|
||||
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Deselect(store.state.items[0])).join()
|
||||
|
||||
assertFalse(store.state.items[0].selected)
|
||||
assertFalse(store.state.items[1].selected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Test deselecting all items in HistoryMetadataGroupFragmentStore`() = runBlocking {
|
||||
val items = listOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem)
|
||||
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.UpdateHistoryItems(items)).join()
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Select(mozillaHistoryMetadataItem)).join()
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.DeselectAll).join()
|
||||
|
||||
assertFalse(store.state.items[0].selected)
|
||||
assertFalse(store.state.items[1].selected)
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
/* 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.historymetadata.controller
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.ext.directionsEq
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentAction
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
|
||||
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentStore
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class HistoryMetadataGroupControllerTest {
|
||||
|
||||
private val testDispatcher = TestCoroutineDispatcher()
|
||||
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
|
||||
|
||||
private val activity: HomeActivity = mockk(relaxed = true)
|
||||
private val store: HistoryMetadataGroupFragmentStore = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
|
||||
private val mozillaHistoryMetadataItem = History.Metadata(
|
||||
id = 0,
|
||||
title = "Mozilla",
|
||||
url = "mozilla.org",
|
||||
visitedAt = 0,
|
||||
totalViewTime = 1
|
||||
)
|
||||
private val firefoxHistoryMetadataItem = History.Metadata(
|
||||
id = 0,
|
||||
title = "Firefox",
|
||||
url = "firefox.com",
|
||||
visitedAt = 0,
|
||||
totalViewTime = 1
|
||||
)
|
||||
|
||||
private lateinit var controller: DefaultHistoryMetadataGroupController
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
controller = DefaultHistoryMetadataGroupController(
|
||||
activity = activity,
|
||||
store = store,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleOpen() {
|
||||
controller.handleOpen(mozillaHistoryMetadataItem)
|
||||
|
||||
verify {
|
||||
activity.openToBrowserAndLoad(
|
||||
searchTermOrURL = mozillaHistoryMetadataItem.url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromHistoryMetadataGroup,
|
||||
historyMetadata = mozillaHistoryMetadataItem.historyMetadataKey
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleSelect() {
|
||||
controller.handleSelect(mozillaHistoryMetadataItem)
|
||||
|
||||
verify {
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Select(mozillaHistoryMetadataItem))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleDeselect() {
|
||||
controller.handleDeselect(mozillaHistoryMetadataItem)
|
||||
|
||||
verify {
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.Deselect(mozillaHistoryMetadataItem))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleBackPressed() {
|
||||
assertTrue(controller.handleBackPressed(setOf(mozillaHistoryMetadataItem)))
|
||||
|
||||
verify {
|
||||
store.dispatch(HistoryMetadataGroupFragmentAction.DeselectAll)
|
||||
}
|
||||
|
||||
assertFalse(controller.handleBackPressed(emptySet()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun handleShare() {
|
||||
controller.handleShare(setOf(mozillaHistoryMetadataItem, firefoxHistoryMetadataItem))
|
||||
|
||||
val data = arrayOf(
|
||||
ShareData(
|
||||
title = mozillaHistoryMetadataItem.title,
|
||||
url = mozillaHistoryMetadataItem.url
|
||||
),
|
||||
ShareData(
|
||||
title = firefoxHistoryMetadataItem.title,
|
||||
url = firefoxHistoryMetadataItem.url
|
||||
),
|
||||
)
|
||||
|
||||
verify {
|
||||
navController.navigate(
|
||||
directionsEq(HistoryMetadataGroupFragmentDirections.actionGlobalShareFragment(data))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/* 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.historymetadata.view
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import androidx.navigation.Navigation
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.support.test.robolectric.testContext
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.databinding.HistoryMetadataGroupListItemBinding
|
||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||
import org.mozilla.fenix.library.history.History
|
||||
import org.mozilla.fenix.library.historymetadata.interactor.HistoryMetadataGroupInteractor
|
||||
import org.mozilla.fenix.selection.SelectionHolder
|
||||
|
||||
@RunWith(FenixRobolectricTestRunner::class)
|
||||
class HistoryMetadataGroupItemViewHolderTest {
|
||||
|
||||
private lateinit var binding: HistoryMetadataGroupListItemBinding
|
||||
private lateinit var interactor: HistoryMetadataGroupInteractor
|
||||
private lateinit var selectionHolder: SelectionHolder<History.Metadata>
|
||||
|
||||
private val item = History.Metadata(
|
||||
id = 0,
|
||||
title = "Mozilla",
|
||||
url = "mozilla.org",
|
||||
visitedAt = 0,
|
||||
totalViewTime = 0
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
binding = HistoryMetadataGroupListItemBinding.inflate(LayoutInflater.from(testContext))
|
||||
Navigation.setViewNavController(binding.root, mockk(relaxed = true))
|
||||
interactor = mockk(relaxed = true)
|
||||
selectionHolder = mockk(relaxed = true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN a history metadata item on bind THEN set the title and url text`() {
|
||||
HistoryMetadataGroupItemViewHolder(binding.root, interactor, selectionHolder).bind(item)
|
||||
|
||||
assertEquals(item.title, binding.historyLayout.titleView.text)
|
||||
assertEquals(item.url, binding.historyLayout.urlView.text)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue