mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-17 15:26:23 +00:00
[fenix] For issue https://github.com/mozilla-mobile/fenix/issues/9949 - Bookmarks/History deletion inconsistencies (https://github.com/mozilla-mobile/fenix/pull/12630)
- Added the undo action for deleting individual history items by creating a new field to the history state containing the id's of the history items that are pending for deletion; This field is used inside the update function from the view to show/hide the items. - Added a new check inside the "deleteMulti" method from BookmarkFragment that calls the showRemoveFoldersDialog to prevent the user from being able to delete one or more bookmark folders without being asked for confirmation, as in https://github.com/mozilla-mobile/fenix/issues/8648. Co-authored-by: Mihai Eduard Badea <mihai.badea@softvision.ro>
This commit is contained in:
parent
549acfa6f1
commit
65c98d6c16
@ -159,6 +159,7 @@ class HistoryTest {
|
||||
}.openHistory {
|
||||
}.openThreeDotMenu {
|
||||
}.clickDelete {
|
||||
verifyDeleteSnackbarText("Deleted")
|
||||
verifyEmptyHistoryView()
|
||||
}
|
||||
}
|
||||
@ -175,6 +176,7 @@ class HistoryTest {
|
||||
clickDeleteHistoryButton()
|
||||
verifyDeleteConfirmationMessage()
|
||||
confirmDeleteAllHistory()
|
||||
verifyDeleteSnackbarText("Browsing data deleted")
|
||||
verifyEmptyHistoryView()
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +82,8 @@ class HistoryRobot {
|
||||
.click()
|
||||
}
|
||||
|
||||
fun verifyDeleteSnackbarText(text: String) = assertSnackBarText(text)
|
||||
|
||||
class Transition {
|
||||
fun closeMenu(interact: HistoryRobot.() -> Unit): Transition {
|
||||
closeButton().click()
|
||||
@ -158,3 +160,6 @@ private fun assertDeleteConfirmationMessage() =
|
||||
.check(matches(isDisplayed()))
|
||||
|
||||
private fun assertCopySnackBarText() = snackBarText().check(matches(withText("URL copied")))
|
||||
|
||||
private fun assertSnackBarText(text: String) =
|
||||
snackBarText().check(matches(withText(Matchers.containsString(text))))
|
||||
|
@ -42,7 +42,7 @@ interface BookmarkController {
|
||||
fun handleBookmarkSharing(item: BookmarkNode)
|
||||
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
|
||||
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event)
|
||||
fun handleBookmarkFolderDeletion(node: BookmarkNode)
|
||||
fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>)
|
||||
fun handleRequestSync()
|
||||
fun handleBackPressed()
|
||||
}
|
||||
@ -58,7 +58,7 @@ class DefaultBookmarkController(
|
||||
private val loadBookmarkNode: suspend (String) -> BookmarkNode?,
|
||||
private val showSnackbar: (String) -> Unit,
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit,
|
||||
private val deleteBookmarkFolder: (BookmarkNode) -> Unit,
|
||||
private val deleteBookmarkFolder: (Set<BookmarkNode>) -> Unit,
|
||||
private val invokePendingDeletion: () -> Unit
|
||||
) : BookmarkController {
|
||||
|
||||
@ -133,8 +133,8 @@ class DefaultBookmarkController(
|
||||
deleteBookmarkNodes(nodes, eventType)
|
||||
}
|
||||
|
||||
override fun handleBookmarkFolderDeletion(node: BookmarkNode) {
|
||||
deleteBookmarkFolder(node)
|
||||
override fun handleBookmarkFolderDeletion(nodes: Set<BookmarkNode>) {
|
||||
deleteBookmarkFolder(nodes)
|
||||
}
|
||||
|
||||
override fun handleRequestSync() {
|
||||
|
@ -283,13 +283,17 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||
}
|
||||
|
||||
private fun deleteMulti(selected: Set<BookmarkNode>, eventType: Event = Event.RemoveBookmarks) {
|
||||
selected.forEach { if (it.type == BookmarkNodeType.FOLDER) {
|
||||
showRemoveFolderDialog(selected)
|
||||
return
|
||||
} }
|
||||
updatePendingBookmarksToDelete(selected)
|
||||
|
||||
pendingBookmarkDeletionJob = getDeleteOperation(eventType)
|
||||
|
||||
val message = when (eventType) {
|
||||
is Event.RemoveBookmarks -> {
|
||||
getRemoveBookmarksSnackBarMessage(selected)
|
||||
getRemoveBookmarksSnackBarMessage(selected, containsFolders = false)
|
||||
}
|
||||
is Event.RemoveBookmarkFolder,
|
||||
is Event.RemoveBookmark -> {
|
||||
@ -310,9 +314,16 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||
)
|
||||
}
|
||||
|
||||
private fun getRemoveBookmarksSnackBarMessage(selected: Set<BookmarkNode>): String {
|
||||
private fun getRemoveBookmarksSnackBarMessage(
|
||||
selected: Set<BookmarkNode>,
|
||||
containsFolders: Boolean
|
||||
): String {
|
||||
return if (selected.size > 1) {
|
||||
getString(R.string.bookmark_deletion_multiple_snackbar_message_2)
|
||||
return if (containsFolders) {
|
||||
getString(R.string.bookmark_deletion_multiple_snackbar_message_3)
|
||||
} else {
|
||||
getString(R.string.bookmark_deletion_multiple_snackbar_message_2)
|
||||
}
|
||||
} else {
|
||||
val bookmarkNode = selected.first()
|
||||
getString(
|
||||
@ -323,29 +334,38 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||
}
|
||||
}
|
||||
|
||||
private fun getDialogConfirmationMessage(selected: Set<BookmarkNode>): String {
|
||||
return if (selected.size > 1) {
|
||||
getString(R.string.bookmark_delete_multiple_folders_confirmation_dialog, getString(R.string.app_name))
|
||||
} else {
|
||||
getString(R.string.bookmark_delete_folder_confirmation_dialog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_bookmarkInteractor = null
|
||||
}
|
||||
|
||||
private fun showRemoveFolderDialog(selected: BookmarkNode) {
|
||||
private fun showRemoveFolderDialog(selected: Set<BookmarkNode>) {
|
||||
activity?.let { activity ->
|
||||
AlertDialog.Builder(activity).apply {
|
||||
setMessage(R.string.bookmark_delete_folder_confirmation_dialog)
|
||||
val dialogConfirmationMessage = getDialogConfirmationMessage(selected)
|
||||
setMessage(dialogConfirmationMessage)
|
||||
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { dialog: DialogInterface, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
|
||||
updatePendingBookmarksToDelete(setOf(selected))
|
||||
updatePendingBookmarksToDelete(selected)
|
||||
pendingBookmarkDeletionJob = getDeleteOperation(Event.RemoveBookmarkFolder)
|
||||
dialog.dismiss()
|
||||
val message = getDeleteDialogString(selected)
|
||||
val snackbarMessage = getRemoveBookmarksSnackBarMessage(selected, containsFolders = true)
|
||||
viewLifecycleOwner.lifecycleScope.allowUndo(
|
||||
requireView(),
|
||||
message,
|
||||
snackbarMessage,
|
||||
getString(R.string.bookmark_undo_deletion),
|
||||
{
|
||||
undoPendingDeletion(setOf(selected))
|
||||
undoPendingDeletion(selected)
|
||||
},
|
||||
operation = getDeleteOperation(Event.RemoveBookmarkFolder)
|
||||
)
|
||||
@ -362,14 +382,6 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||
bookmarkInteractor.onBookmarksChanged(bookmarkTree)
|
||||
}
|
||||
|
||||
private fun getDeleteDialogString(selected: BookmarkNode): String {
|
||||
return getString(
|
||||
R.string.bookmark_deletion_snackbar_message,
|
||||
context?.components?.publicSuffixList?.let { selected.url?.toShortUrl(it) }
|
||||
?: selected.title
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun undoPendingDeletion(selected: Set<BookmarkNode>) {
|
||||
pendingBookmarksToDelete.removeAll(selected)
|
||||
pendingBookmarkDeletionJob = null
|
||||
|
@ -88,7 +88,7 @@ class BookmarkFragmentInteractor(
|
||||
null -> Event.RemoveBookmarks
|
||||
}
|
||||
if (eventType == Event.RemoveBookmarkFolder) {
|
||||
bookmarksController.handleBookmarkFolderDeletion(nodes.first())
|
||||
bookmarksController.handleBookmarkFolderDeletion(nodes)
|
||||
} else {
|
||||
bookmarksController.handleBookmarkDeletion(nodes, eventType)
|
||||
}
|
||||
|
@ -33,6 +33,8 @@ class HistoryAdapter(
|
||||
|
||||
private var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
|
||||
override val selectedItems get() = mode.selectedItems
|
||||
var pendingDeletionIds = emptySet<Long>()
|
||||
private val itemsWithHeaders: MutableMap<HistoryItemTimeGroup, Int> = mutableMapOf()
|
||||
|
||||
override fun getItemViewType(position: Int): Int = HistoryListItemViewHolder.LAYOUT_ID
|
||||
|
||||
@ -48,13 +50,33 @@ class HistoryAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HistoryListItemViewHolder, position: Int) {
|
||||
val previous = if (position == 0) null else getItem(position - 1)
|
||||
val current = getItem(position) ?: return
|
||||
val headerForCurrentItem = timeGroupForHistoryItem(current)
|
||||
val isPendingDeletion = pendingDeletionIds.contains(current.visitedAt)
|
||||
var timeGroup: HistoryItemTimeGroup? = null
|
||||
|
||||
val previousHeader = previous?.let(::timeGroupForHistoryItem)
|
||||
val currentHeader = timeGroupForHistoryItem(current)
|
||||
val timeGroup = if (currentHeader != previousHeader) currentHeader else null
|
||||
holder.bind(current, timeGroup, position == 0, mode)
|
||||
// Add or remove the header and position to the map depending on it's deletion status
|
||||
if (itemsWithHeaders.containsKey(headerForCurrentItem)) {
|
||||
if (isPendingDeletion && itemsWithHeaders[headerForCurrentItem] == position) {
|
||||
itemsWithHeaders.remove(headerForCurrentItem)
|
||||
} else if (isPendingDeletion && itemsWithHeaders[headerForCurrentItem] != position) {
|
||||
// do nothing
|
||||
} else {
|
||||
if (position <= itemsWithHeaders[headerForCurrentItem] as Int) {
|
||||
itemsWithHeaders[headerForCurrentItem] = position
|
||||
timeGroup = headerForCurrentItem
|
||||
}
|
||||
}
|
||||
} else if (!isPendingDeletion) {
|
||||
itemsWithHeaders[headerForCurrentItem] = position
|
||||
timeGroup = headerForCurrentItem
|
||||
}
|
||||
|
||||
holder.bind(current, timeGroup, position == 0, mode, isPendingDeletion)
|
||||
}
|
||||
|
||||
fun updatePendingDeletionIds(pendingDeletionIds: Set<Long>) {
|
||||
this.pendingDeletionIds = pendingDeletionIds
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -17,8 +17,11 @@ import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavDirections
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import kotlinx.android.synthetic.main.fragment_history.view.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
@ -31,7 +34,6 @@ import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.addons.showSnackBar
|
||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.components.history.createSynchronousPagedHistoryProvider
|
||||
@ -42,6 +44,7 @@ import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.ext.toShortUrl
|
||||
import org.mozilla.fenix.library.LibraryPageFragment
|
||||
import org.mozilla.fenix.utils.allowUndo
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandler {
|
||||
@ -49,6 +52,8 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
private lateinit var historyView: HistoryView
|
||||
private lateinit var historyInteractor: HistoryInteractor
|
||||
private lateinit var viewModel: HistoryViewModel
|
||||
private var undoScope: CoroutineScope? = null
|
||||
private var pendingHistoryDeletionJob: (suspend () -> Unit)? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@ -59,7 +64,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
historyStore = StoreProvider.get(this) {
|
||||
HistoryFragmentStore(
|
||||
HistoryFragmentState(
|
||||
items = listOf(), mode = HistoryFragmentState.Mode.Normal
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Normal,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -111,18 +119,18 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
}
|
||||
|
||||
private fun deleteHistoryItems(items: Set<HistoryItem>) {
|
||||
val message = getMultiSelectSnackBarMessage(items)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
context?.components?.run {
|
||||
for (item in items) {
|
||||
analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
core.historyStorage.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
}
|
||||
viewModel.invalidate()
|
||||
showSnackBar(requireView(), message)
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
}
|
||||
|
||||
updatePendingHistoryToDelete(items)
|
||||
undoScope = CoroutineScope(IO)
|
||||
undoScope?.allowUndo(
|
||||
requireView(),
|
||||
getMultiSelectSnackBarMessage(items),
|
||||
getString(R.string.bookmark_undo_deletion),
|
||||
{
|
||||
undoPendingDeletion(items)
|
||||
},
|
||||
getDeleteHistoryItemsOperation(items)
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@ -146,8 +154,8 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
val menuRes = when (historyStore.state.mode) {
|
||||
HistoryFragmentState.Mode.Normal -> R.menu.library_menu
|
||||
is HistoryFragmentState.Mode.Syncing -> R.menu.library_menu
|
||||
is HistoryFragmentState.Mode.Editing -> R.menu.history_select_multi
|
||||
else -> return
|
||||
}
|
||||
|
||||
inflater.inflate(menuRes, menu)
|
||||
@ -166,13 +174,8 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
true
|
||||
}
|
||||
R.id.delete_history_multi_select -> {
|
||||
val message = getMultiSelectSnackBarMessage(selectedItems)
|
||||
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||
deleteSelectedHistory(historyStore.state.mode.selectedItems, requireComponents)
|
||||
viewModel.invalidate()
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
showSnackBar(requireView(), message)
|
||||
}
|
||||
deleteHistoryItems(historyStore.state.mode.selectedItems)
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitEditMode)
|
||||
true
|
||||
}
|
||||
R.id.open_history_in_new_tabs_multi_select -> {
|
||||
@ -181,8 +184,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
selectedItem.url
|
||||
}
|
||||
|
||||
nav(
|
||||
R.id.historyFragment,
|
||||
navigate(
|
||||
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
true
|
||||
@ -197,8 +199,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
browsingModeManager.mode = BrowsingMode.Private
|
||||
supportActionBar?.hide()
|
||||
}
|
||||
nav(
|
||||
R.id.historyFragment,
|
||||
navigate(
|
||||
HistoryFragmentDirections.actionGlobalTabTrayDialogFragment()
|
||||
)
|
||||
true
|
||||
@ -218,7 +219,15 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean = historyView.onBackPressed()
|
||||
override fun onPause() {
|
||||
invokePendingDeletion()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
invokePendingDeletion()
|
||||
return historyView.onBackPressed()
|
||||
}
|
||||
|
||||
private fun openItem(item: HistoryItem, mode: BrowsingMode? = null) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
|
||||
@ -258,23 +267,58 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deleteSelectedHistory(
|
||||
selected: Set<HistoryItem>,
|
||||
components: Components = requireComponents
|
||||
) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
val storage = components.core.historyStorage
|
||||
for (item in selected) {
|
||||
storage.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(data: List<ShareData>) {
|
||||
requireComponents.analytics.metrics.track(Event.HistoryItemShared)
|
||||
val directions = HistoryFragmentDirections.actionGlobalShareFragment(
|
||||
data = data.toTypedArray()
|
||||
)
|
||||
nav(R.id.historyFragment, directions)
|
||||
navigate(directions)
|
||||
}
|
||||
|
||||
private fun navigate(directions: NavDirections) {
|
||||
invokePendingDeletion()
|
||||
findNavController().nav(
|
||||
R.id.historyFragment,
|
||||
directions
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDeleteHistoryItemsOperation(items: Set<HistoryItem>): (suspend () -> Unit) {
|
||||
return {
|
||||
CoroutineScope(IO).launch {
|
||||
historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode)
|
||||
context?.components?.run {
|
||||
for (item in items) {
|
||||
analytics.metrics.track(Event.HistoryItemRemoved)
|
||||
core.historyStorage.deleteVisit(item.url, item.visitedAt)
|
||||
}
|
||||
}
|
||||
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
|
||||
pendingHistoryDeletionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePendingHistoryToDelete(items: Set<HistoryItem>) {
|
||||
pendingHistoryDeletionJob = getDeleteHistoryItemsOperation(items)
|
||||
val ids = items.map { item -> item.visitedAt }.toSet()
|
||||
historyStore.dispatch(HistoryFragmentAction.AddPendingDeletionSet(ids))
|
||||
}
|
||||
|
||||
private fun undoPendingDeletion(items: Set<HistoryItem>) {
|
||||
pendingHistoryDeletionJob = null
|
||||
val ids = items.map { item -> item.visitedAt }.toSet()
|
||||
historyStore.dispatch(HistoryFragmentAction.UndoPendingDeletionSet(ids))
|
||||
}
|
||||
|
||||
private fun invokePendingDeletion() {
|
||||
pendingHistoryDeletionJob?.let {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
it.invoke()
|
||||
}.invokeOnCompletion {
|
||||
pendingHistoryDeletionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun syncHistory() {
|
||||
|
@ -30,6 +30,8 @@ sealed class HistoryFragmentAction : Action {
|
||||
object ExitEditMode : HistoryFragmentAction()
|
||||
data class AddItemForRemoval(val item: HistoryItem) : HistoryFragmentAction()
|
||||
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryFragmentAction()
|
||||
data class AddPendingDeletionSet(val itemIds: Set<Long>) : HistoryFragmentAction()
|
||||
data class UndoPendingDeletionSet(val itemIds: Set<Long>) : HistoryFragmentAction()
|
||||
object EnterDeletionMode : HistoryFragmentAction()
|
||||
object ExitDeletionMode : HistoryFragmentAction()
|
||||
object StartSync : HistoryFragmentAction()
|
||||
@ -41,12 +43,16 @@ sealed class HistoryFragmentAction : Action {
|
||||
* @property items List of HistoryItem to display
|
||||
* @property mode Current Mode of History
|
||||
*/
|
||||
data class HistoryFragmentState(val items: List<HistoryItem>, val mode: Mode) : State {
|
||||
data class HistoryFragmentState(
|
||||
val items: List<HistoryItem>,
|
||||
val mode: Mode,
|
||||
val pendingDeletionIds: Set<Long>,
|
||||
val isDeletingItems: Boolean
|
||||
) : State {
|
||||
sealed class Mode {
|
||||
open val selectedItems = emptySet<HistoryItem>()
|
||||
|
||||
object Normal : Mode()
|
||||
object Deleting : Mode()
|
||||
object Syncing : Mode()
|
||||
data class Editing(override val selectedItems: Set<HistoryItem>) : Mode()
|
||||
}
|
||||
@ -73,9 +79,17 @@ private fun historyStateReducer(
|
||||
)
|
||||
}
|
||||
is HistoryFragmentAction.ExitEditMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
|
||||
is HistoryFragmentAction.EnterDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Deleting)
|
||||
is HistoryFragmentAction.ExitDeletionMode -> state.copy(mode = HistoryFragmentState.Mode.Normal)
|
||||
is HistoryFragmentAction.EnterDeletionMode -> state.copy(isDeletingItems = true)
|
||||
is HistoryFragmentAction.ExitDeletionMode -> state.copy(isDeletingItems = false)
|
||||
is HistoryFragmentAction.StartSync -> state.copy(mode = HistoryFragmentState.Mode.Syncing)
|
||||
is HistoryFragmentAction.FinishSync -> state.copy(mode = HistoryFragmentState.Mode.Normal)
|
||||
is HistoryFragmentAction.AddPendingDeletionSet ->
|
||||
state.copy(
|
||||
pendingDeletionIds = state.pendingDeletionIds + action.itemIds
|
||||
)
|
||||
is HistoryFragmentAction.UndoPendingDeletionSet ->
|
||||
state.copy(
|
||||
pendingDeletionIds = state.pendingDeletionIds - action.itemIds
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -90,7 +90,6 @@ class HistoryView(
|
||||
val view: View = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_history, container, true)
|
||||
|
||||
private var items: List<HistoryItem> = listOf()
|
||||
var mode: HistoryFragmentState.Mode = HistoryFragmentState.Mode.Normal
|
||||
private set
|
||||
|
||||
@ -116,13 +115,16 @@ class HistoryView(
|
||||
fun update(state: HistoryFragmentState) {
|
||||
val oldMode = mode
|
||||
|
||||
view.progress_bar.isVisible = state.mode === HistoryFragmentState.Mode.Deleting
|
||||
view.progress_bar.isVisible = state.isDeletingItems
|
||||
view.swipe_refresh.isRefreshing = state.mode === HistoryFragmentState.Mode.Syncing
|
||||
view.swipe_refresh.isEnabled =
|
||||
state.mode === HistoryFragmentState.Mode.Normal || state.mode === HistoryFragmentState.Mode.Syncing
|
||||
items = state.items
|
||||
mode = state.mode
|
||||
|
||||
historyAdapter.updatePendingDeletionIds(state.pendingDeletionIds)
|
||||
|
||||
updateEmptyState(state.pendingDeletionIds.size != historyAdapter.currentList?.size)
|
||||
|
||||
historyAdapter.updateMode(state.mode)
|
||||
val first = layoutManager.findFirstVisibleItemPosition()
|
||||
val last = layoutManager.findLastVisibleItemPosition() + 1
|
||||
|
@ -11,13 +11,13 @@ import kotlinx.android.synthetic.main.library_site_item.view.*
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.hideAndDisable
|
||||
import org.mozilla.fenix.ext.showAndEnable
|
||||
import org.mozilla.fenix.utils.Do
|
||||
import org.mozilla.fenix.library.SelectionHolder
|
||||
import org.mozilla.fenix.library.history.HistoryFragmentState
|
||||
import org.mozilla.fenix.library.history.HistoryInteractor
|
||||
import org.mozilla.fenix.library.history.HistoryItem
|
||||
import org.mozilla.fenix.library.history.HistoryItemMenu
|
||||
import org.mozilla.fenix.library.history.HistoryItemTimeGroup
|
||||
import org.mozilla.fenix.utils.Do
|
||||
|
||||
class HistoryListItemViewHolder(
|
||||
view: View,
|
||||
@ -44,8 +44,15 @@ class HistoryListItemViewHolder(
|
||||
item: HistoryItem,
|
||||
timeGroup: HistoryItemTimeGroup?,
|
||||
showDeleteButton: Boolean,
|
||||
mode: HistoryFragmentState.Mode
|
||||
mode: HistoryFragmentState.Mode,
|
||||
isPendingDeletion: Boolean = false
|
||||
) {
|
||||
if (isPendingDeletion) {
|
||||
itemView.history_layout.visibility = View.GONE
|
||||
} else {
|
||||
itemView.history_layout.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
itemView.history_layout.titleView.text = item.title
|
||||
itemView.history_layout.urlView.text = item.url
|
||||
|
||||
|
@ -566,6 +566,8 @@
|
||||
<string name="bookmark_select_folder">Select folder</string>
|
||||
<!-- Confirmation message for a dialog confirming if the user wants to delete the selected folder -->
|
||||
<string name="bookmark_delete_folder_confirmation_dialog">Are you sure you want to delete this folder?</string>
|
||||
<!-- Confirmation message for a dialog confirming if the user wants to delete multiple items including folders. Parameter will be replaced by app name. -->
|
||||
<string name="bookmark_delete_multiple_folders_confirmation_dialog">%s will delete the selected items.</string>
|
||||
<!-- Snackbar title shown after a folder has been deleted. This first parameter is the name of the deleted folder -->
|
||||
<string name="bookmark_delete_folder_snackbar">Deleted %1$s</string>
|
||||
<!-- Screen title for adding a bookmarks folder -->
|
||||
@ -620,8 +622,10 @@
|
||||
<!-- Bookmark snackbar message on deletion
|
||||
The first parameter is the host part of the URL of the bookmark deleted, if any -->
|
||||
<string name="bookmark_deletion_snackbar_message">Deleted %1$s</string>
|
||||
<!-- Bookmark snackbar message on deleting multiple bookmarks -->
|
||||
<!-- Bookmark snackbar message on deleting multiple bookmarks not including folders-->
|
||||
<string name="bookmark_deletion_multiple_snackbar_message_2">Bookmarks deleted</string>
|
||||
<!-- Bookmark snackbar message on deleting multiple bookmarks including folders-->
|
||||
<string name="bookmark_deletion_multiple_snackbar_message_3">Deleting selected folders</string>
|
||||
<!-- Bookmark undo button for deletion snackbar action -->
|
||||
<string name="bookmark_undo_deletion">UNDO</string>
|
||||
|
||||
|
@ -55,7 +55,7 @@ class BookmarkControllerTest {
|
||||
private val loadBookmarkNode: suspend (String) -> BookmarkNode? = mockk(relaxed = true)
|
||||
private val showSnackbar: (String) -> Unit = mockk(relaxed = true)
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
||||
private val deleteBookmarkFolder: (BookmarkNode) -> Unit = mockk(relaxed = true)
|
||||
private val deleteBookmarkFolder: (Set<BookmarkNode>) -> Unit = mockk(relaxed = true)
|
||||
private val invokePendingDeletion: () -> Unit = mockk(relaxed = true)
|
||||
|
||||
private val homeActivity: HomeActivity = mockk(relaxed = true)
|
||||
@ -304,10 +304,10 @@ class BookmarkControllerTest {
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkDeletion for a folder should properly call the delete folder delegate`() {
|
||||
controller.handleBookmarkFolderDeletion(subfolder)
|
||||
controller.handleBookmarkFolderDeletion(setOf(subfolder))
|
||||
|
||||
verify {
|
||||
deleteBookmarkFolder(subfolder)
|
||||
deleteBookmarkFolder(setOf(subfolder))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +180,7 @@ class BookmarkFragmentInteractorTest {
|
||||
interactor.onDelete(setOf(subfolder))
|
||||
|
||||
verify {
|
||||
bookmarkController.handleBookmarkFolderDeletion(subfolder)
|
||||
bookmarkController.handleBookmarkFolderDeletion(setOf(subfolder))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,9 @@ class HistoryFragmentStoreTest {
|
||||
fun finishSync() = runBlocking {
|
||||
val initialState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Syncing
|
||||
mode = HistoryFragmentState.Mode.Syncing,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
val store = HistoryFragmentStore(initialState)
|
||||
|
||||
@ -71,16 +73,22 @@ class HistoryFragmentStoreTest {
|
||||
|
||||
private fun emptyDefaultState(): HistoryFragmentState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Normal
|
||||
mode = HistoryFragmentState.Mode.Normal,
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
|
||||
private fun oneItemEditState(): HistoryFragmentState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem))
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem)),
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
|
||||
private fun twoItemEditState(): HistoryFragmentState = HistoryFragmentState(
|
||||
items = listOf(),
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem, newHistoryItem))
|
||||
mode = HistoryFragmentState.Mode.Editing(setOf(historyItem, newHistoryItem)),
|
||||
pendingDeletionIds = emptySet(),
|
||||
isDeletingItems = false
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user