4
4
5
5
package org.mozilla.fenix.library.history
6
6
7
+ import android.R.id.undo
7
8
import android.app.Dialog
8
9
import android.content.DialogInterface
9
10
import android.content.Intent
@@ -58,14 +59,21 @@ import androidx.paging.Pager
58
59
import androidx.paging.PagingConfig
59
60
import androidx.paging.PagingData
60
61
import com.google.android.material.dialog.MaterialAlertDialogBuilder
62
+ import kotlinx.coroutines.CoroutineScope
63
+ import kotlinx.coroutines.Dispatchers
64
+ import kotlinx.coroutines.Dispatchers.IO
61
65
import kotlinx.coroutines.flow.Flow
62
66
import kotlinx.coroutines.flow.mapNotNull
63
67
import kotlinx.coroutines.launch
68
+ import kotlinx.coroutines.withContext
64
69
import mozilla.components.browser.state.action.AwesomeBarAction
65
70
import mozilla.components.browser.state.action.AwesomeBarAction.EngagementFinished
66
71
import mozilla.components.browser.state.action.EngineAction
72
+ import mozilla.components.browser.state.action.HistoryMetadataAction
67
73
import mozilla.components.browser.state.action.RecentlyClosedAction
68
74
import mozilla.components.browser.state.state.searchEngines
75
+ import mozilla.components.browser.state.store.BrowserStore
76
+ import mozilla.components.browser.storage.sync.PlacesHistoryStorage
69
77
import mozilla.components.compose.base.theme.AcornTheme
70
78
import mozilla.components.compose.base.utils.BackInvokedHandler
71
79
import mozilla.components.compose.browser.awesomebar.AwesomeBar
@@ -85,13 +93,15 @@ import mozilla.components.lib.state.ext.observeAsComposableState
85
93
import mozilla.components.support.base.feature.UserInteractionHandler
86
94
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
87
95
import mozilla.components.support.ktx.android.view.hideKeyboard
96
+ import mozilla.components.support.ktx.kotlin.toShortUrl
88
97
import mozilla.components.ui.widgets.withCenterAlignedButtons
89
98
import mozilla.telemetry.glean.private.NoExtras
90
99
import org.mozilla.fenix.HomeActivity
91
100
import org.mozilla.fenix.NavHostActivity
92
101
import org.mozilla.fenix.R
93
102
import org.mozilla.fenix.addons.showSnackBar
94
103
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
104
+ import org.mozilla.fenix.components.AppStore
95
105
import org.mozilla.fenix.components.QrScanFenixFeature
96
106
import org.mozilla.fenix.components.StoreProvider
97
107
import org.mozilla.fenix.components.VoiceSearchFeature
@@ -102,6 +112,7 @@ import org.mozilla.fenix.components.search.HISTORY_SEARCH_ENGINE_ID
102
112
import org.mozilla.fenix.components.toolbar.BrowserToolbarEnvironment
103
113
import org.mozilla.fenix.databinding.FragmentHistoryBinding
104
114
import org.mozilla.fenix.ext.components
115
+ import org.mozilla.fenix.ext.getRootView
105
116
import org.mozilla.fenix.ext.nav
106
117
import org.mozilla.fenix.ext.pixelSizeFor
107
118
import org.mozilla.fenix.ext.requireComponents
@@ -128,6 +139,7 @@ import org.mozilla.fenix.search.SearchFragmentStore
128
139
import org.mozilla.fenix.search.createInitialSearchFragmentState
129
140
import org.mozilla.fenix.tabstray.Page
130
141
import org.mozilla.fenix.theme.FirefoxTheme
142
+ import org.mozilla.fenix.utils.allowUndo
131
143
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
132
144
133
145
private const val MATERIAL_DESIGN_SCRIM = " #52000000"
@@ -242,6 +254,28 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
242
254
GleanHistory .opened.record(NoExtras ())
243
255
}
244
256
257
+ private fun showDeleteSnackbar (
258
+ items : Set <History >,
259
+ ) {
260
+ val appStore = requireComponents.appStore
261
+ val browserStore = requireComponents.core.store
262
+ val historyStorage = requireComponents.core.historyStorage
263
+
264
+ CoroutineScope (Dispatchers .Main ).allowUndo(
265
+ view = requireActivity().getRootView()!! ,
266
+ message = getMultiSelectSnackBarMessage(items),
267
+ undoActionTitle = getString(R .string.snackbar_deleted_undo),
268
+ onCancel = { undo(appStore = appStore, items = items) },
269
+ operation = {
270
+ delete(
271
+ browserStore = browserStore,
272
+ historyStorage = historyStorage,
273
+ items = items,
274
+ )
275
+ },
276
+ )
277
+ }
278
+
245
279
private fun onTimeFrameDeleted () {
246
280
runIfFragmentIsAttached {
247
281
historyView.historyAdapter.refresh()
@@ -587,6 +621,23 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
587
621
}
588
622
}
589
623
624
+ private fun getMultiSelectSnackBarMessage (historyItems : Set <History >): String {
625
+ return if (historyItems.size > 1 ) {
626
+ getString(R .string.history_delete_multiple_items_snackbar)
627
+ } else {
628
+ val historyItem = historyItems.first()
629
+
630
+ String .format(
631
+ requireContext().getString(R .string.history_delete_single_item_snackbar),
632
+ if (historyItem is History .Regular ) {
633
+ historyItem.url.toShortUrl(requireComponents.publicSuffixList)
634
+ } else {
635
+ historyItem.title
636
+ },
637
+ )
638
+ }
639
+ }
640
+
590
641
override fun onBackPressed (): Boolean {
591
642
// The state needs to be updated accordingly if Edit mode is active
592
643
return if (historyStore.state.mode is HistoryFragmentState .Mode .Editing ) {
@@ -635,6 +686,7 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
635
686
val appStore = requireContext().components.appStore
636
687
637
688
appStore.dispatch(AppAction .AddPendingDeletionSet (items.toPendingDeletionHistory()))
689
+ showDeleteSnackbar(items)
638
690
}
639
691
640
692
private fun share (data : List <ShareData >) {
@@ -659,6 +711,34 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
659
711
)
660
712
}
661
713
714
+ private suspend fun undo (appStore : AppStore , items : Set <History >) = withContext(IO ) {
715
+ val pendingDeletionItems = items.map { it.toPendingDeletionHistory() }.toSet()
716
+ appStore.dispatch(AppAction .UndoPendingDeletionSet (pendingDeletionItems))
717
+ }
718
+
719
+ private suspend fun delete (
720
+ browserStore : BrowserStore ,
721
+ historyStorage : PlacesHistoryStorage ,
722
+ items : Set <History >,
723
+ ) = withContext(IO ) {
724
+ historyStore.dispatch(HistoryFragmentAction .EnterDeletionMode )
725
+ for (item in items) {
726
+ when (item) {
727
+ is History .Regular -> historyStorage.deleteVisitsFor(item.url)
728
+ is History .Group -> {
729
+ // NB: If we have non-search groups, this logic needs to be updated.
730
+ historyProvider.deleteMetadataSearchGroup(item)
731
+ browserStore.dispatch(
732
+ HistoryMetadataAction .DisbandSearchGroupAction (searchTerm = item.title),
733
+ )
734
+ }
735
+ // We won't encounter individual metadata entries outside of groups.
736
+ is History .Metadata -> {}
737
+ }
738
+ }
739
+ historyStore.dispatch(HistoryFragmentAction .ExitDeletionMode )
740
+ }
741
+
662
742
private fun onDeleteTimeRange (selectedTimeFrame : RemoveTimeFrame ? ) {
663
743
historyStore.dispatch(HistoryFragmentAction .DeleteTimeRange (selectedTimeFrame))
664
744
historyStore.dispatch(HistoryFragmentAction .EnterDeletionMode )
0 commit comments