For #3987 - Convert History to Lib-State and add tests
parent
9ab67557cf
commit
ae3d187909
|
@ -10,7 +10,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder
|
import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder
|
||||||
import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder
|
import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder
|
||||||
|
@ -94,9 +93,8 @@ private class HistoryList(val history: List<HistoryItem>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HistoryAdapter(
|
class HistoryAdapter(private val historyInteractor: HistoryInteractor) :
|
||||||
private val actionEmitter: Observer<HistoryAction>
|
AdapterWithJob<RecyclerView.ViewHolder>() {
|
||||||
) : AdapterWithJob<RecyclerView.ViewHolder>() {
|
|
||||||
private var historyList: HistoryList = HistoryList(emptyList())
|
private var historyList: HistoryList = HistoryList(emptyList())
|
||||||
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
private var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||||
var selected = listOf<HistoryItem>()
|
var selected = listOf<HistoryItem>()
|
||||||
|
@ -160,9 +158,16 @@ class HistoryAdapter(
|
||||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||||
|
|
||||||
return when (viewType) {
|
return when (viewType) {
|
||||||
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, actionEmitter)
|
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(
|
||||||
|
view,
|
||||||
|
historyInteractor
|
||||||
|
)
|
||||||
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
|
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
|
||||||
HistoryListItemViewHolder.LAYOUT_ID -> HistoryListItemViewHolder(view, actionEmitter, adapterJob)
|
HistoryListItemViewHolder.LAYOUT_ID -> HistoryListItemViewHolder(
|
||||||
|
view,
|
||||||
|
historyInteractor,
|
||||||
|
adapterJob
|
||||||
|
)
|
||||||
else -> throw IllegalStateException()
|
else -> throw IllegalStateException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,107 +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.view.ViewGroup
|
|
||||||
import org.mozilla.fenix.mvi.ViewState
|
|
||||||
import org.mozilla.fenix.mvi.Change
|
|
||||||
import org.mozilla.fenix.mvi.Action
|
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.UIComponent
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
|
||||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
|
||||||
import org.mozilla.fenix.test.Mockable
|
|
||||||
|
|
||||||
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
|
|
||||||
|
|
||||||
@Mockable
|
|
||||||
class HistoryComponent(
|
|
||||||
private val container: ViewGroup,
|
|
||||||
bus: ActionBusFactory,
|
|
||||||
viewModelProvider: UIComponentViewModelProvider<HistoryState, HistoryChange>
|
|
||||||
) :
|
|
||||||
UIComponent<HistoryState, HistoryAction, HistoryChange>(
|
|
||||||
bus.getManagedEmitter(HistoryAction::class.java),
|
|
||||||
bus.getSafeManagedObservable(HistoryChange::class.java),
|
|
||||||
viewModelProvider
|
|
||||||
) {
|
|
||||||
|
|
||||||
override fun initView() = HistoryUIView(container, actionEmitter, changesObservable)
|
|
||||||
|
|
||||||
init {
|
|
||||||
bind()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : ViewState {
|
|
||||||
sealed class Mode {
|
|
||||||
object Normal : Mode()
|
|
||||||
data class Editing(val selectedItems: List<HistoryItem>) : Mode()
|
|
||||||
object Deleting : Mode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class HistoryAction : Action {
|
|
||||||
data class Open(val item: HistoryItem) : HistoryAction()
|
|
||||||
data class EnterEditMode(val item: HistoryItem) : HistoryAction()
|
|
||||||
object BackPressed : HistoryAction()
|
|
||||||
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
|
|
||||||
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
|
|
||||||
object SwitchMode : HistoryAction()
|
|
||||||
|
|
||||||
sealed class Delete : HistoryAction() {
|
|
||||||
object All : Delete()
|
|
||||||
data class One(val item: HistoryItem) : Delete()
|
|
||||||
data class Some(val items: List<HistoryItem>) : Delete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class HistoryChange : Change {
|
|
||||||
data class Change(val list: List<HistoryItem>) : HistoryChange()
|
|
||||||
data class EnterEditMode(val item: HistoryItem) : HistoryChange()
|
|
||||||
object ExitEditMode : HistoryChange()
|
|
||||||
data class AddItemForRemoval(val item: HistoryItem) : HistoryChange()
|
|
||||||
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange()
|
|
||||||
object EnterDeletionMode : HistoryChange()
|
|
||||||
object ExitDeletionMode : HistoryChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistoryViewModel(
|
|
||||||
initialState: HistoryState
|
|
||||||
) : UIComponentViewModelBase<HistoryState, HistoryChange>(initialState, reducer) {
|
|
||||||
companion object {
|
|
||||||
fun create() = HistoryViewModel(HistoryState(emptyList(), HistoryState.Mode.Normal))
|
|
||||||
val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
|
|
||||||
when (change) {
|
|
||||||
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
|
|
||||||
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
|
|
||||||
is HistoryChange.AddItemForRemoval -> {
|
|
||||||
val mode = state.mode
|
|
||||||
if (mode is HistoryState.Mode.Editing) {
|
|
||||||
val items = mode.selectedItems + listOf(change.item)
|
|
||||||
state.copy(mode = mode.copy(selectedItems = items))
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is HistoryChange.RemoveItemForRemoval -> {
|
|
||||||
var mode = state.mode
|
|
||||||
|
|
||||||
if (mode is HistoryState.Mode.Editing) {
|
|
||||||
val items = mode.selectedItems.filter { it.id != change.item.id }
|
|
||||||
mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing(items)
|
|
||||||
|
|
||||||
state.copy(mode = mode)
|
|
||||||
} else {
|
|
||||||
state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
|
|
||||||
is HistoryChange.EnterDeletionMode -> state.copy(mode = HistoryState.Mode.Deleting)
|
|
||||||
is HistoryChange.ExitDeletionMode -> state.copy(mode = HistoryState.Mode.Normal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.whenStarted
|
||||||
import androidx.navigation.Navigation
|
import androidx.navigation.Navigation
|
||||||
import kotlinx.android.synthetic.main.fragment_history.view.*
|
import kotlinx.android.synthetic.main.fragment_history.view.*
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -26,45 +27,55 @@ import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import mozilla.components.concept.storage.VisitType
|
import mozilla.components.concept.storage.VisitType
|
||||||
|
import mozilla.components.lib.state.ext.observe
|
||||||
import mozilla.components.support.base.feature.BackHandler
|
import mozilla.components.support.base.feature.BackHandler
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.BrowsingModeManager
|
import org.mozilla.fenix.BrowsingModeManager
|
||||||
import org.mozilla.fenix.FenixViewModelProvider
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.components.Components
|
import org.mozilla.fenix.components.Components
|
||||||
|
import org.mozilla.fenix.components.StoreProvider
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.getHostFromUrl
|
import org.mozilla.fenix.ext.getHostFromUrl
|
||||||
import org.mozilla.fenix.ext.nav
|
import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.ext.requireComponents
|
import org.mozilla.fenix.ext.requireComponents
|
||||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
|
||||||
import org.mozilla.fenix.mvi.getAutoDisposeObservable
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
import org.mozilla.fenix.share.ShareTab
|
import org.mozilla.fenix.share.ShareTab
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class HistoryFragment : Fragment(), BackHandler {
|
class HistoryFragment : Fragment(), BackHandler {
|
||||||
|
private lateinit var historyStore: HistoryStore
|
||||||
private lateinit var historyComponent: HistoryComponent
|
private lateinit var historyView: HistoryView
|
||||||
private val navigation by lazy { Navigation.findNavController(requireView()) }
|
private lateinit var historyInteractor: HistoryInteractor
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? = inflater
|
): View? {
|
||||||
.inflate(R.layout.fragment_history, container, false).also { view ->
|
val view = inflater.inflate(R.layout.fragment_history, container, false)
|
||||||
historyComponent = HistoryComponent(
|
historyStore = StoreProvider.get(
|
||||||
view.history_layout,
|
|
||||||
ActionBusFactory.get(this),
|
|
||||||
FenixViewModelProvider.create(
|
|
||||||
this,
|
this,
|
||||||
HistoryViewModel::class.java,
|
HistoryStore(
|
||||||
HistoryViewModel.Companion::create
|
HistoryState(
|
||||||
|
items = listOf(), mode = HistoryState.Mode.Normal
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
historyInteractor = HistoryInteractor(
|
||||||
|
historyStore,
|
||||||
|
::openItem,
|
||||||
|
::displayDeleteAllDialog,
|
||||||
|
::invalidateOptionsMenu,
|
||||||
|
::deleteHistoryItems
|
||||||
|
)
|
||||||
|
historyView = HistoryView(view.history_layout, historyInteractor)
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun invalidateOptionsMenu() {
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -74,16 +85,28 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteHistoryItems(items: List<HistoryItem>) {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val storage = context?.components?.core?.historyStorage
|
||||||
|
for (item in items) {
|
||||||
|
storage?.deleteVisit(item.url, item.visitedAt)
|
||||||
|
}
|
||||||
|
reloadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
lifecycleScope.launch { reloadData() }
|
historyStore.observe(view) {
|
||||||
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
whenStarted {
|
||||||
|
historyView.update(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
lifecycleScope.launch { reloadData() }
|
||||||
super.onStart()
|
|
||||||
getAutoDisposeObservable<HistoryAction>()
|
|
||||||
.subscribe(this::handleNewHistoryAction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -95,7 +118,7 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
val mode = (historyComponent.uiView as HistoryUIView).mode
|
val mode = historyStore.state.mode
|
||||||
when (mode) {
|
when (mode) {
|
||||||
HistoryState.Mode.Normal ->
|
HistoryState.Mode.Normal ->
|
||||||
R.menu.library_menu
|
R.menu.library_menu
|
||||||
|
@ -117,7 +140,8 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
|
||||||
R.id.share_history_multi_select -> {
|
R.id.share_history_multi_select -> {
|
||||||
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
|
val selectedHistory =
|
||||||
|
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||||
when {
|
when {
|
||||||
selectedHistory.size == 1 ->
|
selectedHistory.size == 1 ->
|
||||||
share(selectedHistory.first().url)
|
share(selectedHistory.first().url)
|
||||||
|
@ -135,7 +159,8 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
}
|
}
|
||||||
R.id.delete_history_multi_select -> {
|
R.id.delete_history_multi_select -> {
|
||||||
val components = context?.applicationContext?.components!!
|
val components = context?.applicationContext?.components!!
|
||||||
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
|
val selectedHistory =
|
||||||
|
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||||
|
|
||||||
lifecycleScope.launch(Main) {
|
lifecycleScope.launch(Main) {
|
||||||
deleteSelectedHistory(selectedHistory, components)
|
deleteSelectedHistory(selectedHistory, components)
|
||||||
|
@ -144,7 +169,8 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.open_history_in_new_tabs_multi_select -> {
|
R.id.open_history_in_new_tabs_multi_select -> {
|
||||||
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
|
val selectedHistory =
|
||||||
|
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||||
requireComponents.useCases.tabsUseCases.addTab.let { useCase ->
|
requireComponents.useCases.tabsUseCases.addTab.let { useCase ->
|
||||||
for (selectedItem in selectedHistory) {
|
for (selectedItem in selectedHistory) {
|
||||||
useCase.invoke(selectedItem.url)
|
useCase.invoke(selectedItem.url)
|
||||||
|
@ -155,11 +181,15 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
browsingModeManager.mode = BrowsingModeManager.Mode.Normal
|
browsingModeManager.mode = BrowsingModeManager.Mode.Normal
|
||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
}
|
}
|
||||||
nav(R.id.historyFragment, HistoryFragmentDirections.actionHistoryFragmentToHomeFragment())
|
nav(
|
||||||
|
R.id.historyFragment,
|
||||||
|
HistoryFragmentDirections.actionHistoryFragmentToHomeFragment()
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.open_history_in_private_tabs_multi_select -> {
|
R.id.open_history_in_private_tabs_multi_select -> {
|
||||||
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected()
|
val selectedHistory =
|
||||||
|
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
|
||||||
requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase ->
|
requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase ->
|
||||||
for (selectedItem in selectedHistory) {
|
for (selectedItem in selectedHistory) {
|
||||||
useCase.invoke(selectedItem.url)
|
useCase.invoke(selectedItem.url)
|
||||||
|
@ -170,47 +200,18 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
browsingModeManager.mode = BrowsingModeManager.Mode.Private
|
browsingModeManager.mode = BrowsingModeManager.Mode.Private
|
||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
}
|
}
|
||||||
nav(R.id.historyFragment, HistoryFragmentDirections.actionHistoryFragmentToHomeFragment())
|
nav(
|
||||||
|
R.id.historyFragment,
|
||||||
|
HistoryFragmentDirections.actionHistoryFragmentToHomeFragment()
|
||||||
|
)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean = (historyComponent.uiView as HistoryUIView).onBackPressed()
|
override fun onBackPressed(): Boolean = historyView.onBackPressed()
|
||||||
|
|
||||||
private fun handleNewHistoryAction(action: HistoryAction) {
|
fun openItem(item: HistoryItem) {
|
||||||
when (action) {
|
|
||||||
is HistoryAction.Open ->
|
|
||||||
openItem(action.item)
|
|
||||||
is HistoryAction.EnterEditMode ->
|
|
||||||
emitChange { HistoryChange.EnterEditMode(action.item) }
|
|
||||||
is HistoryAction.AddItemForRemoval ->
|
|
||||||
emitChange { HistoryChange.AddItemForRemoval(action.item) }
|
|
||||||
is HistoryAction.RemoveItemForRemoval ->
|
|
||||||
emitChange { HistoryChange.RemoveItemForRemoval(action.item) }
|
|
||||||
is HistoryAction.BackPressed ->
|
|
||||||
emitChange { HistoryChange.ExitEditMode }
|
|
||||||
is HistoryAction.Delete.All ->
|
|
||||||
displayDeleteAllDialog()
|
|
||||||
is HistoryAction.Delete.One -> lifecycleScope.launch {
|
|
||||||
requireComponents.core
|
|
||||||
.historyStorage
|
|
||||||
.deleteVisit(action.item.url, action.item.visitedAt)
|
|
||||||
reloadData()
|
|
||||||
}
|
|
||||||
is HistoryAction.Delete.Some -> lifecycleScope.launch {
|
|
||||||
val storage = requireComponents.core.historyStorage
|
|
||||||
for (item in action.items) {
|
|
||||||
storage.deleteVisit(item.url, item.visitedAt)
|
|
||||||
}
|
|
||||||
reloadData()
|
|
||||||
}
|
|
||||||
is HistoryAction.SwitchMode ->
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openItem(item: HistoryItem) {
|
|
||||||
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
|
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
|
||||||
(activity as HomeActivity).openToBrowserAndLoad(
|
(activity as HomeActivity).openToBrowserAndLoad(
|
||||||
searchTermOrURL = item.url,
|
searchTermOrURL = item.url,
|
||||||
|
@ -219,7 +220,7 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayDeleteAllDialog() {
|
fun displayDeleteAllDialog() {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
AlertDialog.Builder(activity).apply {
|
AlertDialog.Builder(activity).apply {
|
||||||
setMessage(R.string.history_delete_all_dialog)
|
setMessage(R.string.history_delete_all_dialog)
|
||||||
|
@ -227,13 +228,13 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
dialog.cancel()
|
dialog.cancel()
|
||||||
}
|
}
|
||||||
setPositiveButton(R.string.history_clear_dialog) { dialog: DialogInterface, _ ->
|
setPositiveButton(R.string.history_clear_dialog) { dialog: DialogInterface, _ ->
|
||||||
emitChange { HistoryChange.EnterDeletionMode }
|
historyStore.dispatch(HistoryAction.EnterDeletionMode)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
|
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
|
||||||
requireComponents.core.historyStorage.deleteEverything()
|
requireComponents.core.historyStorage.deleteEverything()
|
||||||
reloadData()
|
reloadData()
|
||||||
launch(Dispatchers.Main) {
|
launch(Dispatchers.Main) {
|
||||||
emitChange { HistoryChange.ExitDeletionMode }
|
historyStore.dispatch(HistoryAction.ExitDeletionMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,7 +276,7 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
.toList()
|
.toList()
|
||||||
|
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
emitChange { HistoryChange.Change(items) }
|
historyStore.dispatch(HistoryAction.Change(items))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,10 +301,6 @@ class HistoryFragment : Fragment(), BackHandler {
|
||||||
nav(R.id.historyFragment, directions)
|
nav(R.id.historyFragment, directions)
|
||||||
}
|
}
|
||||||
|
|
||||||
private inline fun emitChange(producer: () -> HistoryChange) {
|
|
||||||
getManagedEmitter<HistoryChange>().onNext(producer())
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val HISTORY_TIME_DAYS = 3L
|
private const val HISTORY_TIME_DAYS = 3L
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
/* 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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interactor for the history screen
|
||||||
|
* Provides implementations for the HistoryViewInteractor
|
||||||
|
*/
|
||||||
|
class HistoryInteractor(
|
||||||
|
private val store: HistoryStore,
|
||||||
|
private val openToBrowser: (item: HistoryItem) -> Unit,
|
||||||
|
private val displayDeleteAll: () -> Unit,
|
||||||
|
private val invalidateOptionsMenu: () -> Unit,
|
||||||
|
private val deleteHistoryItems: (List<HistoryItem>) -> Unit
|
||||||
|
) : HistoryViewInteractor {
|
||||||
|
override fun onHistoryItemOpened(item: HistoryItem) {
|
||||||
|
openToBrowser(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnterEditMode(selectedItem: HistoryItem) {
|
||||||
|
store.dispatch(HistoryAction.EnterEditMode(selectedItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
store.dispatch(HistoryAction.ExitEditMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemAddedForRemoval(item: HistoryItem) {
|
||||||
|
store.dispatch(HistoryAction.AddItemForRemoval(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemRemovedForRemoval(item: HistoryItem) {
|
||||||
|
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onModeSwitched() {
|
||||||
|
invalidateOptionsMenu.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteAll() {
|
||||||
|
displayDeleteAll.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteOne(item: HistoryItem) {
|
||||||
|
deleteHistoryItems.invoke(listOf(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteSome(items: List<HistoryItem>) {
|
||||||
|
deleteHistoryItems.invoke(items)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
/* 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 mozilla.components.lib.state.Action
|
||||||
|
import mozilla.components.lib.state.State
|
||||||
|
import mozilla.components.lib.state.Store
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class representing a history entry
|
||||||
|
* @property id Unique id of the history item
|
||||||
|
* @property title Title of the history item
|
||||||
|
* @property url URL of the history item
|
||||||
|
* @property visitedAt Timestamp of when this history item was visited
|
||||||
|
*/
|
||||||
|
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The [Store] for holding the [HistoryState] and applying [HistoryAction]s.
|
||||||
|
*/
|
||||||
|
class HistoryStore(initialState: HistoryState) :
|
||||||
|
Store<HistoryState, HistoryAction>(initialState, ::historyStateReducer)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to dispatch through the `HistoryStore` to modify `HistoryState` through the reducer.
|
||||||
|
*/
|
||||||
|
sealed class HistoryAction : Action {
|
||||||
|
data class Change(val list: List<HistoryItem>) : HistoryAction()
|
||||||
|
data class EnterEditMode(val item: HistoryItem) : HistoryAction()
|
||||||
|
object ExitEditMode : HistoryAction()
|
||||||
|
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
|
||||||
|
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
|
||||||
|
object EnterDeletionMode : HistoryAction()
|
||||||
|
object ExitDeletionMode : HistoryAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state for the History Screen
|
||||||
|
* @property items List of HistoryItem to display
|
||||||
|
* @property mode Current Mode of History
|
||||||
|
*/
|
||||||
|
data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State {
|
||||||
|
sealed class Mode {
|
||||||
|
object Normal : Mode()
|
||||||
|
data class Editing(val selectedItems: List<HistoryItem>) : Mode()
|
||||||
|
object Deleting : Mode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HistoryState Reducer.
|
||||||
|
*/
|
||||||
|
fun historyStateReducer(state: HistoryState, action: HistoryAction): HistoryState {
|
||||||
|
return when (action) {
|
||||||
|
is HistoryAction.Change -> state.copy(mode = HistoryState.Mode.Normal, items = action.list)
|
||||||
|
is HistoryAction.EnterEditMode -> state.copy(
|
||||||
|
mode = HistoryState.Mode.Editing(listOf(action.item))
|
||||||
|
)
|
||||||
|
is HistoryAction.AddItemForRemoval -> {
|
||||||
|
val mode = state.mode
|
||||||
|
if (mode is HistoryState.Mode.Editing) {
|
||||||
|
val items = mode.selectedItems + listOf(action.item)
|
||||||
|
state.copy(mode = HistoryState.Mode.Editing(items))
|
||||||
|
} else {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is HistoryAction.RemoveItemForRemoval -> {
|
||||||
|
var mode = state.mode
|
||||||
|
|
||||||
|
if (mode is HistoryState.Mode.Editing) {
|
||||||
|
val items = mode.selectedItems.filter { it.id != action.item.id }
|
||||||
|
mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing(
|
||||||
|
items
|
||||||
|
)
|
||||||
|
|
||||||
|
state.copy(mode = mode)
|
||||||
|
} else {
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is HistoryAction.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
|
||||||
|
is HistoryAction.EnterDeletionMode -> state.copy(mode = HistoryState.Mode.Deleting)
|
||||||
|
is HistoryAction.ExitDeletionMode -> state.copy(mode = HistoryState.Mode.Normal)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,102 +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.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import io.reactivex.Observable
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.functions.Consumer
|
|
||||||
import kotlinx.android.synthetic.main.component_history.*
|
|
||||||
import kotlinx.android.synthetic.main.component_history.view.*
|
|
||||||
import kotlinx.android.synthetic.main.delete_history_button.*
|
|
||||||
import mozilla.components.support.base.feature.BackHandler
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.ext.getColorIntFromAttr
|
|
||||||
import org.mozilla.fenix.library.LibraryPageUIView
|
|
||||||
|
|
||||||
class HistoryUIView(
|
|
||||||
container: ViewGroup,
|
|
||||||
actionEmitter: Observer<HistoryAction>,
|
|
||||||
changesObservable: Observable<HistoryChange>
|
|
||||||
) :
|
|
||||||
LibraryPageUIView<HistoryState, HistoryAction, HistoryChange>(container, actionEmitter, changesObservable),
|
|
||||||
BackHandler {
|
|
||||||
|
|
||||||
var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
|
||||||
private set
|
|
||||||
|
|
||||||
private val historyAdapter: HistoryAdapter
|
|
||||||
private var items: List<HistoryItem> = listOf()
|
|
||||||
|
|
||||||
fun getSelected(): List<HistoryItem> = historyAdapter.selected
|
|
||||||
|
|
||||||
override val view: ConstraintLayout = LayoutInflater.from(container.context)
|
|
||||||
.inflate(R.layout.component_history, container, true)
|
|
||||||
.findViewById(R.id.history_wrapper)
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.history_list.apply {
|
|
||||||
historyAdapter = HistoryAdapter(actionEmitter)
|
|
||||||
adapter = historyAdapter
|
|
||||||
layoutManager = LinearLayoutManager(container.context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateView() = Consumer<HistoryState> {
|
|
||||||
view.progress_bar.visibility = if (it.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
if (it.mode != mode) {
|
|
||||||
mode = it.mode
|
|
||||||
actionEmitter.onNext(HistoryAction.SwitchMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
(view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode)
|
|
||||||
|
|
||||||
items = it.items
|
|
||||||
when (val modeCopy = mode) {
|
|
||||||
is HistoryState.Mode.Normal -> setUIForNormalMode(items.isEmpty())
|
|
||||||
is HistoryState.Mode.Editing -> setUIForSelectingMode(modeCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUIForSelectingMode(
|
|
||||||
mode: HistoryState.Mode.Editing
|
|
||||||
) {
|
|
||||||
activity?.title =
|
|
||||||
context.getString(R.string.history_multi_select_title, mode.selectedItems.size)
|
|
||||||
setToolbarColors(
|
|
||||||
R.color.white_color,
|
|
||||||
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUIForNormalMode(isEmpty: Boolean) {
|
|
||||||
activity?.title = context.getString(R.string.library_history)
|
|
||||||
delete_history_button?.isVisible = !isEmpty
|
|
||||||
history_empty_view.isVisible = isEmpty
|
|
||||||
setToolbarColors(
|
|
||||||
R.attr.primaryText.getColorIntFromAttr(context!!),
|
|
||||||
R.attr.foundation.getColorIntFromAttr(context)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
|
||||||
return when (mode) {
|
|
||||||
is HistoryState.Mode.Editing -> {
|
|
||||||
mode = HistoryState.Mode.Normal
|
|
||||||
historyAdapter.updateData(items, mode)
|
|
||||||
setUIForNormalMode(items.isEmpty())
|
|
||||||
actionEmitter.onNext(HistoryAction.BackPressed)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
/* 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.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffColorFilter
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageButton
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.component_history.*
|
||||||
|
import kotlinx.android.synthetic.main.component_history.view.*
|
||||||
|
import kotlinx.android.synthetic.main.delete_history_button.*
|
||||||
|
import mozilla.components.support.base.feature.BackHandler
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.ext.asActivity
|
||||||
|
import org.mozilla.fenix.ext.getColorIntFromAttr
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the HistoryViewInteractor. This interface is implemented by objects that want
|
||||||
|
* to respond to user interaction on the HistoryView
|
||||||
|
*/
|
||||||
|
interface HistoryViewInteractor {
|
||||||
|
/**
|
||||||
|
* Called whenever a history item is tapped to open that history entry in the browser
|
||||||
|
* @param item the history item to open in browser
|
||||||
|
*/
|
||||||
|
fun onHistoryItemOpened(item: HistoryItem)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a history item is long pressed and edit mode is launched
|
||||||
|
* @param selectedItem the history item to start selected for deletion in edit mode
|
||||||
|
*/
|
||||||
|
fun onEnterEditMode(selectedItem: HistoryItem)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on backpressed to exit edit mode
|
||||||
|
*/
|
||||||
|
fun onBackPressed()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a history item is tapped in edit mode and added for removal
|
||||||
|
* @param item the history item to add to selected items for deletion in edit mode
|
||||||
|
*/
|
||||||
|
fun onItemAddedForRemoval(item: HistoryItem)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a selected history item is tapped in edit mode and removed from removal
|
||||||
|
* @param item the history item to remove from the selected items for deletion in edit mode
|
||||||
|
*/
|
||||||
|
fun onItemRemovedForRemoval(item: HistoryItem)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the mode is switched so we can invalidate the menu
|
||||||
|
*/
|
||||||
|
fun onModeSwitched()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when delete all is tapped
|
||||||
|
*/
|
||||||
|
fun onDeleteAll()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when one history item is deleted
|
||||||
|
* @param item the history item to delete
|
||||||
|
*/
|
||||||
|
fun onDeleteOne(item: HistoryItem)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when multiple history items are deleted
|
||||||
|
* @param items the history items to delete
|
||||||
|
*/
|
||||||
|
fun onDeleteSome(items: List<HistoryItem>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View that contains and configures the History List
|
||||||
|
*/
|
||||||
|
class HistoryView(
|
||||||
|
private val container: ViewGroup,
|
||||||
|
val interactor: HistoryInteractor
|
||||||
|
) : LayoutContainer, BackHandler {
|
||||||
|
|
||||||
|
val view: ConstraintLayout = LayoutInflater.from(container.context)
|
||||||
|
.inflate(R.layout.component_history, container, true)
|
||||||
|
.findViewById(R.id.history_wrapper)
|
||||||
|
|
||||||
|
override val containerView: View?
|
||||||
|
get() = container
|
||||||
|
|
||||||
|
private val historyAdapter: HistoryAdapter
|
||||||
|
private var items: List<HistoryItem> = listOf()
|
||||||
|
private val context = container.context
|
||||||
|
var mode: HistoryState.Mode = HistoryState.Mode.Normal
|
||||||
|
private set
|
||||||
|
private val activity = context?.asActivity()
|
||||||
|
|
||||||
|
init {
|
||||||
|
view.history_list.apply {
|
||||||
|
historyAdapter = HistoryAdapter(interactor)
|
||||||
|
adapter = historyAdapter
|
||||||
|
layoutManager = LinearLayoutManager(container.context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(state: HistoryState) {
|
||||||
|
view.progress_bar.visibility =
|
||||||
|
if (state.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
if (state.mode != mode) {
|
||||||
|
mode = state.mode
|
||||||
|
interactor.onModeSwitched()
|
||||||
|
}
|
||||||
|
|
||||||
|
(view.history_list.adapter as HistoryAdapter).updateData(state.items, state.mode)
|
||||||
|
|
||||||
|
items = state.items
|
||||||
|
when (val mode = mode) {
|
||||||
|
is HistoryState.Mode.Normal -> setUIForNormalMode(items.isEmpty())
|
||||||
|
is HistoryState.Mode.Editing -> setUIForSelectingMode(mode.selectedItems.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUIForSelectingMode(selectedItemSize: Int) {
|
||||||
|
activity?.title =
|
||||||
|
context.getString(R.string.history_multi_select_title, selectedItemSize)
|
||||||
|
setToolbarColors(
|
||||||
|
R.color.white_color,
|
||||||
|
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUIForNormalMode(isEmpty: Boolean) {
|
||||||
|
activity?.title = context.getString(R.string.library_history)
|
||||||
|
delete_history_button?.isVisible = !isEmpty
|
||||||
|
history_empty_view.isVisible = isEmpty
|
||||||
|
setToolbarColors(
|
||||||
|
R.attr.primaryText.getColorIntFromAttr(context!!),
|
||||||
|
R.attr.foundation.getColorIntFromAttr(context)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setToolbarColors(foreground: Int, background: Int) {
|
||||||
|
val toolbar = (activity as AppCompatActivity).findViewById<Toolbar>(R.id.navigationToolbar)
|
||||||
|
val colorFilter = PorterDuffColorFilter(
|
||||||
|
ContextCompat.getColor(context, foreground),
|
||||||
|
PorterDuff.Mode.SRC_IN
|
||||||
|
)
|
||||||
|
toolbar.setBackgroundColor(ContextCompat.getColor(context, background))
|
||||||
|
toolbar.setTitleTextColor(ContextCompat.getColor(context, foreground))
|
||||||
|
|
||||||
|
themeToolbar(
|
||||||
|
toolbar, foreground,
|
||||||
|
background, colorFilter
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun themeToolbar(
|
||||||
|
toolbar: Toolbar,
|
||||||
|
textColor: Int,
|
||||||
|
backgroundColor: Int,
|
||||||
|
colorFilter: PorterDuffColorFilter? = null
|
||||||
|
) {
|
||||||
|
toolbar.setTitleTextColor(ContextCompat.getColor(context!!, textColor))
|
||||||
|
toolbar.setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
|
||||||
|
|
||||||
|
if (colorFilter == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toolbar.overflowIcon?.colorFilter = colorFilter
|
||||||
|
(0 until toolbar.childCount).forEach {
|
||||||
|
when (val item = toolbar.getChildAt(it)) {
|
||||||
|
is ImageButton -> item.drawable.colorFilter = colorFilter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(): Boolean {
|
||||||
|
return when (mode) {
|
||||||
|
is HistoryState.Mode.Editing -> {
|
||||||
|
mode = HistoryState.Mode.Normal
|
||||||
|
historyAdapter.updateData(items, mode)
|
||||||
|
setUIForNormalMode(items.isEmpty())
|
||||||
|
interactor.onBackPressed()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,15 +6,14 @@ package org.mozilla.fenix.library.history.viewholders
|
||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.delete_history_button.view.*
|
import kotlinx.android.synthetic.main.delete_history_button.view.*
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.library.history.HistoryAction
|
import org.mozilla.fenix.library.history.HistoryInteractor
|
||||||
import org.mozilla.fenix.library.history.HistoryState
|
import org.mozilla.fenix.library.history.HistoryState
|
||||||
|
|
||||||
class HistoryDeleteButtonViewHolder(
|
class HistoryDeleteButtonViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
private val actionEmitter: Observer<HistoryAction>
|
historyInteractor: HistoryInteractor
|
||||||
) : RecyclerView.ViewHolder(view) {
|
) : RecyclerView.ViewHolder(view) {
|
||||||
private var mode: HistoryState.Mode? = null
|
private var mode: HistoryState.Mode? = null
|
||||||
private val buttonView = view.delete_history_button
|
private val buttonView = view.delete_history_button
|
||||||
|
@ -22,13 +21,10 @@ class HistoryDeleteButtonViewHolder(
|
||||||
init {
|
init {
|
||||||
buttonView.setOnClickListener {
|
buttonView.setOnClickListener {
|
||||||
mode?.also {
|
mode?.also {
|
||||||
val action = when (it) {
|
when (it) {
|
||||||
is HistoryState.Mode.Normal -> HistoryAction.Delete.All
|
is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
|
||||||
is HistoryState.Mode.Editing -> HistoryAction.Delete.Some(it.selectedItems)
|
is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
|
||||||
is HistoryState.Mode.Deleting -> null
|
}
|
||||||
} ?: return@also
|
|
||||||
|
|
||||||
actionEmitter.onNext(action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import io.reactivex.Observer
|
|
||||||
import kotlinx.android.synthetic.main.history_list_item.view.*
|
import kotlinx.android.synthetic.main.history_list_item.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -19,7 +18,7 @@ import mozilla.components.browser.menu.BrowserMenu
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.ThemeManager
|
import org.mozilla.fenix.ThemeManager
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.library.history.HistoryAction
|
import org.mozilla.fenix.library.history.HistoryInteractor
|
||||||
import org.mozilla.fenix.library.history.HistoryItem
|
import org.mozilla.fenix.library.history.HistoryItem
|
||||||
import org.mozilla.fenix.library.history.HistoryItemMenu
|
import org.mozilla.fenix.library.history.HistoryItemMenu
|
||||||
import org.mozilla.fenix.library.history.HistoryState
|
import org.mozilla.fenix.library.history.HistoryState
|
||||||
|
@ -27,7 +26,7 @@ import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class HistoryListItemViewHolder(
|
class HistoryListItemViewHolder(
|
||||||
view: View,
|
view: View,
|
||||||
private val actionEmitter: Observer<HistoryAction>,
|
private val historyInteractor: HistoryInteractor,
|
||||||
val job: Job
|
val job: Job
|
||||||
) : RecyclerView.ViewHolder(view), CoroutineScope {
|
) : RecyclerView.ViewHolder(view), CoroutineScope {
|
||||||
|
|
||||||
|
@ -48,13 +47,11 @@ class HistoryListItemViewHolder(
|
||||||
}
|
}
|
||||||
|
|
||||||
item?.apply {
|
item?.apply {
|
||||||
val action = if (isChecked) {
|
if (isChecked) {
|
||||||
HistoryAction.AddItemForRemoval(this)
|
historyInteractor.onItemAddedForRemoval(this)
|
||||||
} else {
|
} else {
|
||||||
HistoryAction.RemoveItemForRemoval(this)
|
historyInteractor.onItemRemovedForRemoval(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
actionEmitter.onNext(action)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +60,7 @@ class HistoryListItemViewHolder(
|
||||||
|
|
||||||
view.setOnLongClickListener {
|
view.setOnLongClickListener {
|
||||||
item?.apply {
|
item?.apply {
|
||||||
actionEmitter.onNext(HistoryAction.EnterEditMode(this))
|
historyInteractor.onEnterEditMode(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
true
|
true
|
||||||
|
@ -72,7 +69,8 @@ class HistoryListItemViewHolder(
|
||||||
menuButton.setOnClickListener {
|
menuButton.setOnClickListener {
|
||||||
historyMenu.menuBuilder.build(view.context).show(
|
historyMenu.menuBuilder.build(view.context).show(
|
||||||
anchor = it,
|
anchor = it,
|
||||||
orientation = BrowserMenu.Orientation.DOWN)
|
orientation = BrowserMenu.Orientation.DOWN
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +95,8 @@ class HistoryListItemViewHolder(
|
||||||
} else {
|
} else {
|
||||||
ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
|
ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
|
||||||
}
|
}
|
||||||
val backgroundTintList = ContextCompat.getColorStateList(itemView.context, backgroundTint)
|
val backgroundTintList =
|
||||||
|
ContextCompat.getColorStateList(itemView.context, backgroundTint)
|
||||||
favicon.backgroundTintList = backgroundTintList
|
favicon.backgroundTintList = backgroundTintList
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
|
@ -107,7 +106,8 @@ class HistoryListItemViewHolder(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
|
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
|
||||||
val backgroundTintList = ContextCompat.getColorStateList(itemView.context, backgroundTint)
|
val backgroundTintList =
|
||||||
|
ContextCompat.getColorStateList(itemView.context, backgroundTint)
|
||||||
favicon.backgroundTintList = backgroundTintList
|
favicon.backgroundTintList = backgroundTintList
|
||||||
updateFavIcon(item.url)
|
updateFavIcon(item.url)
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ class HistoryListItemViewHolder(
|
||||||
this.historyMenu = HistoryItemMenu(itemView.context) {
|
this.historyMenu = HistoryItemMenu(itemView.context) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is HistoryItemMenu.Item.Delete -> {
|
is HistoryItemMenu.Item.Delete -> {
|
||||||
item?.apply { actionEmitter.onNext(HistoryAction.Delete.One(this)) }
|
item?.apply { historyInteractor.onDeleteOne(this) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,11 +139,13 @@ class HistoryListItemViewHolder(
|
||||||
) {
|
) {
|
||||||
itemView.history_layout.setOnClickListener {
|
itemView.history_layout.setOnClickListener {
|
||||||
if (mode == HistoryState.Mode.Normal) {
|
if (mode == HistoryState.Mode.Normal) {
|
||||||
actionEmitter.onNext(HistoryAction.Open(item))
|
historyInteractor.onHistoryItemOpened(item)
|
||||||
} else {
|
} else {
|
||||||
if (selected) actionEmitter.onNext(HistoryAction.RemoveItemForRemoval(item)) else actionEmitter.onNext(
|
if (selected) {
|
||||||
HistoryAction.AddItemForRemoval(item)
|
historyInteractor.onItemRemovedForRemoval(item)
|
||||||
)
|
} else {
|
||||||
|
historyInteractor.onItemAddedForRemoval(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
/* 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 io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HistoryInteractorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onHistoryItemOpened() {
|
||||||
|
var historyItemReceived: HistoryItem? = null
|
||||||
|
val historyItem = HistoryItem(0, "title", "url", 0.toLong())
|
||||||
|
val interactor = HistoryInteractor(
|
||||||
|
mockk(),
|
||||||
|
{ historyItemReceived = it },
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
mockk()
|
||||||
|
)
|
||||||
|
interactor.onHistoryItemOpened(historyItem)
|
||||||
|
assertEquals(historyItem, historyItemReceived)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onEnterEditMode() {
|
||||||
|
val store: HistoryStore = mockk(relaxed = true)
|
||||||
|
val newHistoryItem: HistoryItem = mockk(relaxed = true)
|
||||||
|
val interactor =
|
||||||
|
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
|
||||||
|
interactor.onEnterEditMode(newHistoryItem)
|
||||||
|
verify { store.dispatch(HistoryAction.EnterEditMode(newHistoryItem)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onBackPressed() {
|
||||||
|
val store: HistoryStore = mockk(relaxed = true)
|
||||||
|
val interactor =
|
||||||
|
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
|
||||||
|
interactor.onBackPressed()
|
||||||
|
verify { store.dispatch(HistoryAction.ExitEditMode) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onItemAddedForRemoval() {
|
||||||
|
val store: HistoryStore = mockk(relaxed = true)
|
||||||
|
val newHistoryItem: HistoryItem = mockk(relaxed = true)
|
||||||
|
|
||||||
|
val interactor =
|
||||||
|
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
|
||||||
|
interactor.onItemAddedForRemoval(newHistoryItem)
|
||||||
|
verify { store.dispatch(HistoryAction.AddItemForRemoval(newHistoryItem)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onItemRemovedForRemoval() {
|
||||||
|
val store: HistoryStore = mockk(relaxed = true)
|
||||||
|
val newHistoryItem: HistoryItem = mockk(relaxed = true)
|
||||||
|
val interactor =
|
||||||
|
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
|
||||||
|
interactor.onItemRemovedForRemoval(newHistoryItem)
|
||||||
|
verify { store.dispatch(HistoryAction.RemoveItemForRemoval(newHistoryItem)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onModeSwitched() {
|
||||||
|
var menuInvalidated = false
|
||||||
|
val interactor = HistoryInteractor(
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
{ menuInvalidated = true },
|
||||||
|
mockk()
|
||||||
|
)
|
||||||
|
interactor.onModeSwitched()
|
||||||
|
assertEquals(true, menuInvalidated)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteAll() {
|
||||||
|
var deleteAllDialogShown = false
|
||||||
|
val interactor = HistoryInteractor(
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
{ deleteAllDialogShown = true },
|
||||||
|
mockk(),
|
||||||
|
mockk()
|
||||||
|
)
|
||||||
|
interactor.onDeleteAll()
|
||||||
|
assertEquals(true, deleteAllDialogShown)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteOne() {
|
||||||
|
var itemsToDelete: List<HistoryItem>? = null
|
||||||
|
val historyItem = HistoryItem(0, "title", "url", 0.toLong())
|
||||||
|
val interactor =
|
||||||
|
HistoryInteractor(
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
{ itemsToDelete = it }
|
||||||
|
)
|
||||||
|
interactor.onDeleteOne(historyItem)
|
||||||
|
assertEquals(itemsToDelete, listOf(historyItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun onDeleteSome() {
|
||||||
|
var itemsToDelete: List<HistoryItem>? = null
|
||||||
|
val historyItem = HistoryItem(0, "title", "url", 0.toLong())
|
||||||
|
val newHistoryItem = HistoryItem(1, "title", "url", 0.toLong())
|
||||||
|
val interactor =
|
||||||
|
HistoryInteractor(
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
mockk(),
|
||||||
|
{ itemsToDelete = it }
|
||||||
|
)
|
||||||
|
interactor.onDeleteSome(listOf(historyItem, newHistoryItem))
|
||||||
|
assertEquals(itemsToDelete, listOf(historyItem, newHistoryItem))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
/* 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 kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertNotSame
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class HistoryStoreTest {
|
||||||
|
private val historyItem = HistoryItem(0, "title", "url", 0.toLong())
|
||||||
|
private val newHistoryItem = HistoryItem(1, "title", "url", 0.toLong())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun enterEditMode() = runBlocking {
|
||||||
|
val initialState = emptyDefaultState()
|
||||||
|
val store = HistoryStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(HistoryAction.EnterEditMode(historyItem)).join()
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(store.state.mode, HistoryState.Mode.Editing(listOf(historyItem)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun exitEditMode() = runBlocking {
|
||||||
|
val initialState = oneItemEditState()
|
||||||
|
val store = HistoryStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(HistoryAction.ExitEditMode).join()
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(store.state.mode, HistoryState.Mode.Normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun itemAddedForRemoval() = runBlocking {
|
||||||
|
val initialState = oneItemEditState()
|
||||||
|
val store = HistoryStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(HistoryAction.AddItemForRemoval(newHistoryItem)).join()
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(
|
||||||
|
store.state.mode,
|
||||||
|
HistoryState.Mode.Editing(listOf(historyItem, newHistoryItem))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun removeItemForRemoval() = runBlocking {
|
||||||
|
val initialState = twoItemEditState()
|
||||||
|
val store = HistoryStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(HistoryAction.RemoveItemForRemoval(newHistoryItem)).join()
|
||||||
|
assertNotSame(initialState, store.state)
|
||||||
|
assertEquals(store.state.mode, HistoryState.Mode.Editing(listOf(historyItem)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun emptyDefaultState(): HistoryState = HistoryState(
|
||||||
|
items = listOf(),
|
||||||
|
mode = HistoryState.Mode.Normal
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun oneItemEditState(): HistoryState = HistoryState(
|
||||||
|
items = listOf(),
|
||||||
|
mode = HistoryState.Mode.Editing(listOf(historyItem))
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun twoItemEditState(): HistoryState = HistoryState(
|
||||||
|
items = listOf(),
|
||||||
|
mode = HistoryState.Mode.Editing(listOf(historyItem, newHistoryItem))
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,95 +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 io.mockk.MockKAnnotations
|
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.observers.TestObserver
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.TestUtils.bus
|
|
||||||
import org.mozilla.fenix.TestUtils.owner
|
|
||||||
import org.mozilla.fenix.TestUtils.setRxSchedulers
|
|
||||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
|
||||||
|
|
||||||
class HistoryViewModelTest {
|
|
||||||
|
|
||||||
private lateinit var historyViewModel: HistoryViewModel
|
|
||||||
private lateinit var historyObserver: TestObserver<HistoryState>
|
|
||||||
private lateinit var emitter: Observer<HistoryChange>
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
MockKAnnotations.init(this)
|
|
||||||
setRxSchedulers()
|
|
||||||
|
|
||||||
historyViewModel = HistoryViewModel.create()
|
|
||||||
historyObserver = historyViewModel.state.test()
|
|
||||||
bus.getSafeManagedObservable(HistoryChange::class.java)
|
|
||||||
.subscribe(historyViewModel.changes::onNext)
|
|
||||||
|
|
||||||
emitter = owner.getManagedEmitter()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `select two items for removal, then deselect one, then select it again`() {
|
|
||||||
val historyItem = HistoryItem(1, "Mozilla", "http://mozilla.org", 0)
|
|
||||||
val historyItem2 = HistoryItem(2, "Mozilla", "http://mozilla.org", 0)
|
|
||||||
|
|
||||||
emitter.onNext(HistoryChange.Change(listOf(historyItem, historyItem2)))
|
|
||||||
emitter.onNext(HistoryChange.EnterEditMode(historyItem))
|
|
||||||
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem2))
|
|
||||||
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
|
|
||||||
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem))
|
|
||||||
emitter.onNext(HistoryChange.ExitEditMode)
|
|
||||||
|
|
||||||
historyObserver.assertSubscribed().awaitCount(7).assertNoErrors()
|
|
||||||
.assertValues(
|
|
||||||
HistoryState(listOf(), HistoryState.Mode.Normal),
|
|
||||||
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Normal),
|
|
||||||
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem))),
|
|
||||||
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem, historyItem2))),
|
|
||||||
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem2))),
|
|
||||||
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem2, historyItem))),
|
|
||||||
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Normal)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@Test
|
|
||||||
fun `deselecting all items triggers normal mode`() {
|
|
||||||
val historyItem = HistoryItem(123, "Mozilla", "http://mozilla.org", 0)
|
|
||||||
|
|
||||||
emitter.onNext(HistoryChange.Change(listOf(historyItem)))
|
|
||||||
emitter.onNext(HistoryChange.EnterEditMode(historyItem))
|
|
||||||
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
|
|
||||||
historyObserver.assertSubscribed().awaitCount(6).assertNoErrors()
|
|
||||||
.assertValues(
|
|
||||||
HistoryState(listOf(), HistoryState.Mode.Normal),
|
|
||||||
HistoryState(listOf(historyItem), HistoryState.Mode.Normal),
|
|
||||||
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))),
|
|
||||||
HistoryState(listOf(historyItem), HistoryState.Mode.Normal)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `try making changes when not in edit mode`() {
|
|
||||||
val historyItems = listOf(
|
|
||||||
HistoryItem(1337, "Reddit", "http://reddit.com", 0),
|
|
||||||
HistoryItem(31337, "Haxor", "http://leethaxor.com", 0)
|
|
||||||
)
|
|
||||||
|
|
||||||
emitter.onNext(HistoryChange.Change(historyItems))
|
|
||||||
emitter.onNext(HistoryChange.AddItemForRemoval(historyItems[0]))
|
|
||||||
emitter.onNext(HistoryChange.EnterEditMode(historyItems[0]))
|
|
||||||
emitter.onNext(HistoryChange.ExitEditMode)
|
|
||||||
|
|
||||||
historyObserver.assertSubscribed().awaitCount(4).assertNoErrors()
|
|
||||||
.assertValues(
|
|
||||||
HistoryState(listOf(), HistoryState.Mode.Normal),
|
|
||||||
HistoryState(historyItems, HistoryState.Mode.Normal),
|
|
||||||
HistoryState(historyItems, HistoryState.Mode.Editing(listOf(historyItems[0]))),
|
|
||||||
HistoryState(historyItems, HistoryState.Mode.Normal)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue