Skip to content

Commit 4ece446

Browse files
committed
Bug 1990978 - Bring back the snackbar with Undo button for when a history item is deleted r=android-reviewers,jonalmeida
This patch essentially reverts this change from back in 143: https://phabricator.services.mozilla.com/D255847 The original patch removed the snackbar that was shown when user deleted one or multiple history items from the list. The complexity here lies in the fact that even though we visually remove the item from the list, under the hood we do not actually remove the item(s) until the snackbar is shown and has gone away. Instead, we add the item(s) to a pending list to be deleted "eventually". Since the original patch simply removed the snackbar visually but did not actually change the logic under the hood to perform the deletion immediately, once user restarts the app, all of their presumably deleted history would reappear. Given the severity of this issue I am going to reintroduce the snackbar to resolve this since it's the fastest and safest solution. We can then look into updating the entire logic stack to honor deletions immediately. Differential Revision: https://phabricator.services.mozilla.com/D266738
1 parent 514a56c commit 4ece446

File tree

3 files changed

+83
-2
lines changed

3 files changed

+83
-2
lines changed

mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ class HistoryTest : TestSetup() {
103103
) {
104104
clickDeleteHistoryButton(firstWebPage.url.toString())
105105
}
106+
verifySnackBarText(expectedText = "Deleted")
106107
verifyEmptyHistoryView()
107108
}
108109
}

mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package org.mozilla.fenix.library.history
66

7+
import android.R.id.undo
78
import android.app.Dialog
89
import android.content.DialogInterface
910
import android.content.Intent
@@ -58,14 +59,21 @@ import androidx.paging.Pager
5859
import androidx.paging.PagingConfig
5960
import androidx.paging.PagingData
6061
import com.google.android.material.dialog.MaterialAlertDialogBuilder
62+
import kotlinx.coroutines.CoroutineScope
63+
import kotlinx.coroutines.Dispatchers
64+
import kotlinx.coroutines.Dispatchers.IO
6165
import kotlinx.coroutines.flow.Flow
6266
import kotlinx.coroutines.flow.mapNotNull
6367
import kotlinx.coroutines.launch
68+
import kotlinx.coroutines.withContext
6469
import mozilla.components.browser.state.action.AwesomeBarAction
6570
import mozilla.components.browser.state.action.AwesomeBarAction.EngagementFinished
6671
import mozilla.components.browser.state.action.EngineAction
72+
import mozilla.components.browser.state.action.HistoryMetadataAction
6773
import mozilla.components.browser.state.action.RecentlyClosedAction
6874
import mozilla.components.browser.state.state.searchEngines
75+
import mozilla.components.browser.state.store.BrowserStore
76+
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
6977
import mozilla.components.compose.base.theme.AcornTheme
7078
import mozilla.components.compose.base.utils.BackInvokedHandler
7179
import mozilla.components.compose.browser.awesomebar.AwesomeBar
@@ -85,13 +93,15 @@ import mozilla.components.lib.state.ext.observeAsComposableState
8593
import mozilla.components.support.base.feature.UserInteractionHandler
8694
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
8795
import mozilla.components.support.ktx.android.view.hideKeyboard
96+
import mozilla.components.support.ktx.kotlin.toShortUrl
8897
import mozilla.components.ui.widgets.withCenterAlignedButtons
8998
import mozilla.telemetry.glean.private.NoExtras
9099
import org.mozilla.fenix.HomeActivity
91100
import org.mozilla.fenix.NavHostActivity
92101
import org.mozilla.fenix.R
93102
import org.mozilla.fenix.addons.showSnackBar
94103
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
104+
import org.mozilla.fenix.components.AppStore
95105
import org.mozilla.fenix.components.QrScanFenixFeature
96106
import org.mozilla.fenix.components.StoreProvider
97107
import org.mozilla.fenix.components.VoiceSearchFeature
@@ -102,6 +112,7 @@ import org.mozilla.fenix.components.search.HISTORY_SEARCH_ENGINE_ID
102112
import org.mozilla.fenix.components.toolbar.BrowserToolbarEnvironment
103113
import org.mozilla.fenix.databinding.FragmentHistoryBinding
104114
import org.mozilla.fenix.ext.components
115+
import org.mozilla.fenix.ext.getRootView
105116
import org.mozilla.fenix.ext.nav
106117
import org.mozilla.fenix.ext.pixelSizeFor
107118
import org.mozilla.fenix.ext.requireComponents
@@ -128,6 +139,7 @@ import org.mozilla.fenix.search.SearchFragmentStore
128139
import org.mozilla.fenix.search.createInitialSearchFragmentState
129140
import org.mozilla.fenix.tabstray.Page
130141
import org.mozilla.fenix.theme.FirefoxTheme
142+
import org.mozilla.fenix.utils.allowUndo
131143
import org.mozilla.fenix.GleanMetrics.History as GleanHistory
132144

133145
private const val MATERIAL_DESIGN_SCRIM = "#52000000"
@@ -242,6 +254,28 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
242254
GleanHistory.opened.record(NoExtras())
243255
}
244256

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+
245279
private fun onTimeFrameDeleted() {
246280
runIfFragmentIsAttached {
247281
historyView.historyAdapter.refresh()
@@ -587,6 +621,23 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
587621
}
588622
}
589623

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+
590641
override fun onBackPressed(): Boolean {
591642
// The state needs to be updated accordingly if Edit mode is active
592643
return if (historyStore.state.mode is HistoryFragmentState.Mode.Editing) {
@@ -635,6 +686,7 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
635686
val appStore = requireContext().components.appStore
636687

637688
appStore.dispatch(AppAction.AddPendingDeletionSet(items.toPendingDeletionHistory()))
689+
showDeleteSnackbar(items)
638690
}
639691

640692
private fun share(data: List<ShareData>) {
@@ -659,6 +711,34 @@ class HistoryFragment : LibraryPageFragment<History>(), UserInteractionHandler,
659711
)
660712
}
661713

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+
662742
private fun onDeleteTimeRange(selectedTimeFrame: RemoveTimeFrame?) {
663743
historyStore.dispatch(HistoryFragmentAction.DeleteTimeRange(selectedTimeFrame))
664744
historyStore.dispatch(HistoryFragmentAction.EnterDeletionMode)

mobile/android/fenix/app/src/main/res/values/strings.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1375,9 +1375,9 @@
13751375
<!-- Text for the button to clear all history -->
13761376
<string name="history_delete_all">Delete history</string>
13771377
<!-- Text for the snackbar to confirm that multiple browsing history items has been deleted -->
1378-
<string name="history_delete_multiple_items_snackbar" tools:ignore="UnusedResources" moz:removedIn="143">History Deleted</string>
1378+
<string name="history_delete_multiple_items_snackbar">History Deleted</string>
13791379
<!-- Text for the snackbar to confirm that a single browsing history item has been deleted. %1$s is the shortened URL of the deleted history item. -->
1380-
<string name="history_delete_single_item_snackbar" tools:ignore="UnusedResources" moz:removedIn="143">Deleted %1$s</string>
1380+
<string name="history_delete_single_item_snackbar">Deleted %1$s</string>
13811381
<!-- Context description text for the button to delete a single history item -->
13821382
<string name="history_delete_item">Delete</string>
13831383
<!-- History multi select title in app bar

0 commit comments

Comments
 (0)