Skip to content

Commit 4b2cfaa

Browse files
committed
Add a snackbar with undo after bookmarking
1 parent 24e11ad commit 4b2cfaa

File tree

4 files changed

+126
-63
lines changed

4 files changed

+126
-63
lines changed

app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherActivity.kt

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import android.view.Menu
2424
import android.view.MenuItem
2525
import android.view.MotionEvent
2626
import android.view.View
27-
import android.widget.TextView
28-
import android.widget.Toast
2927
import androidx.activity.OnBackPressedCallback
3028
import androidx.appcompat.app.AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR
3129
import androidx.appcompat.widget.Toolbar
@@ -76,8 +74,6 @@ import com.duckduckgo.common.utils.DispatcherProvider
7674
import com.duckduckgo.di.scopes.ActivityScope
7775
import com.duckduckgo.duckchat.api.DuckChat
7876
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
79-
import com.google.android.material.snackbar.BaseTransientBottomBar
80-
import com.google.android.material.snackbar.Snackbar
8177
import javax.inject.Inject
8278
import kotlin.coroutines.CoroutineContext
8379
import kotlinx.coroutines.CoroutineScope
@@ -442,17 +438,20 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
442438
is Command.ShareLinks -> launchShareMultipleLinkChooser(command.links)
443439
is Command.ShareLink -> launchShareLinkChooser(command.link, command.title)
444440
is Command.BookmarkTabsRequest -> showBookmarkTabsConfirmation(command.tabIds)
445-
is Command.ShowBookmarkToast -> showBookmarkToast(command.numBookmarks)
441+
is Command.ShowUndoBookmarkMessage -> showBookmarkSnackbarWithUndo(command.numBookmarks)
446442
}
447443
}
448444

449-
private fun showBookmarkToast(numBookmarks: Int) {
450-
val message = if (numBookmarks == 0) {
451-
getString(R.string.tabSwitcherBookmarkToastZero)
452-
} else {
453-
resources.getQuantityString(R.plurals.tabSwitcherBookmarkToast, numBookmarks, numBookmarks)
454-
}
455-
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
445+
private fun showBookmarkSnackbarWithUndo(numBookmarks: Int) {
446+
val message = resources.getQuantityString(R.plurals.tabSwitcherBookmarkToast, numBookmarks, numBookmarks)
447+
TabSwitcherSnackbar(
448+
anchorView = toolbar,
449+
message = message,
450+
action = getString(R.string.undoSnackbarAction),
451+
showAction = numBookmarks > 0,
452+
onAction = viewModel::undoBookmarkAction,
453+
onDismiss = viewModel::finishBookmarkAction,
454+
).show()
456455
}
457456

458457
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -610,34 +609,14 @@ class TabSwitcherActivity : DuckDuckGoActivity(), TabSwitcherListener, Coroutine
610609
}
611610

612611
private fun onDeletableTab(tab: TabEntity) {
613-
Snackbar.make(toolbar, getString(R.string.tabClosed), Snackbar.LENGTH_LONG)
614-
.setDuration(3500) // 3.5 seconds
615-
.setAction(R.string.tabClosedUndo) {
616-
// noop, handled in onDismissed callback
617-
}
618-
.addCallback(
619-
object : Snackbar.Callback() {
620-
override fun onDismissed(
621-
transientBottomBar: Snackbar?,
622-
event: Int,
623-
) {
624-
when (event) {
625-
// handle the UNDO action here as we only have one
626-
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_ACTION -> launch { viewModel.undoDeletableTab(tab) }
627-
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_SWIPE,
628-
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_TIMEOUT,
629-
-> launch { viewModel.purgeDeletableTabs() }
630-
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_CONSECUTIVE,
631-
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL,
632-
-> { /* noop */
633-
}
634-
}
635-
}
636-
},
637-
)
638-
.setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE)
639-
.apply { view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text).maxLines = 1 }
640-
.show()
612+
TabSwitcherSnackbar(
613+
anchorView = toolbar,
614+
message = getString(R.string.tabClosed),
615+
action = getString(R.string.tabClosedUndo),
616+
showAction = true,
617+
onAction = { launch { viewModel.undoDeletableTab(tab) } },
618+
onDismiss = { launch { viewModel.purgeDeletableTabs() } },
619+
).show()
641620
}
642621

643622
private fun launchShareLinkChooser(
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.tabs.ui
18+
19+
import android.view.View
20+
import android.widget.TextView
21+
import com.google.android.material.snackbar.BaseTransientBottomBar
22+
import com.google.android.material.snackbar.Snackbar
23+
24+
class TabSwitcherSnackbar(
25+
anchorView: View,
26+
message: String,
27+
private val action: String? = null,
28+
private val showAction: Boolean = false,
29+
private val onAction: () -> Unit = {},
30+
private val onDismiss: () -> Unit = {},
31+
) {
32+
companion object {
33+
private const val SNACKBAR_DISPLAY_TIME_MS = 3500
34+
}
35+
36+
private val snackbar = Snackbar.make(anchorView, message, Snackbar.LENGTH_LONG)
37+
.setDuration(SNACKBAR_DISPLAY_TIME_MS) // 3.5 seconds
38+
.apply {
39+
if (showAction) {
40+
setAction(action) {
41+
// noop, handled in onDismissed callback
42+
}
43+
}
44+
}
45+
.addCallback(
46+
object : Snackbar.Callback() {
47+
override fun onDismissed(
48+
transientBottomBar: Snackbar?,
49+
event: Int,
50+
) {
51+
when (event) {
52+
// handle the UNDO action here as we only have one
53+
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_ACTION -> onAction()
54+
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_SWIPE,
55+
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_TIMEOUT,
56+
-> onDismiss()
57+
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_CONSECUTIVE,
58+
BaseTransientBottomBar.BaseCallback.DISMISS_EVENT_MANUAL,
59+
-> { /* noop */ }
60+
}
61+
}
62+
},
63+
)
64+
.setAnimationMode(BaseTransientBottomBar.ANIMATION_MODE_SLIDE)
65+
.apply { view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text).maxLines = 1 }
66+
67+
fun show() {
68+
snackbar.show()
69+
}
70+
}

app/src/main/java/com/duckduckgo/app/tabs/ui/TabSwitcherViewModel.kt

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ import com.duckduckgo.app.tabs.model.TabSwitcherData.LayoutType.LIST
4040
import com.duckduckgo.app.tabs.ui.TabSwitcherItem.Tab
4141
import com.duckduckgo.app.tabs.ui.TabSwitcherItem.Tab.NormalTab
4242
import com.duckduckgo.app.tabs.ui.TabSwitcherItem.Tab.SelectableTab
43+
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.BookmarkTabsRequest
44+
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLink
45+
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLinks
46+
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowUndoBookmarkMessage
4347
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.BackButtonType.ARROW
4448
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.BackButtonType.CLOSE
4549
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.FabType
4650
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Normal
4751
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.SelectionViewState.Mode.Selection
48-
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.BookmarkTabsRequest
49-
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLink
50-
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShareLinks
51-
import com.duckduckgo.app.tabs.ui.TabSwitcherViewModel.Command.ShowBookmarkToast
5252
import com.duckduckgo.common.utils.DispatcherProvider
5353
import com.duckduckgo.common.utils.SingleLiveEvent
5454
import com.duckduckgo.common.utils.extensions.toBinaryString
@@ -127,13 +127,15 @@ class TabSwitcherViewModel @Inject constructor(
127127
}
128128
}
129129

130+
private val recentlySavedBookmarks = mutableListOf<Bookmark>()
131+
130132
sealed class Command {
131133
data object Close : Command()
132134
data object CloseAllTabsRequest : Command()
133135
data class ShareLink(val link: String, val title: String) : Command()
134136
data class ShareLinks(val links: List<String>) : Command()
135137
data class BookmarkTabsRequest(val tabIds: List<String>) : Command()
136-
data class ShowBookmarkToast(val numBookmarks: Int) : Command()
138+
data class ShowUndoBookmarkMessage(val numBookmarks: Int) : Command()
137139
}
138140

139141
suspend fun onNewTabRequested(fromOverflowMenu: Boolean) {
@@ -260,34 +262,47 @@ class TabSwitcherViewModel @Inject constructor(
260262
}
261263
}
262264

263-
fun onSelectionModeRequested() {
264-
triggerEmptySelectionMode()
265-
}
266-
267-
private fun triggerEmptySelectionMode() {
268-
_selectionViewState.update { it.copy(mode = Selection(emptyList())) }
269-
}
270-
271-
fun onCloseSelectedTabs() {
265+
fun undoBookmarkAction() {
266+
recentlySavedBookmarks.forEach { bookmark ->
267+
viewModelScope.launch(dispatcherProvider.io()) {
268+
savedSitesRepository.delete(bookmark)
269+
}
270+
}
271+
recentlySavedBookmarks.clear()
272272
}
273273

274-
fun onCloseOtherTabs() {
274+
fun finishBookmarkAction() {
275+
recentlySavedBookmarks.clear()
275276
}
276277

277278
fun onBookmarkTabsConfirmed(tabIds: List<String>) {
278279
viewModelScope.launch {
279-
val numBookmarkedTabs = bookmarkTabs(tabIds)
280-
command.value = ShowBookmarkToast(numBookmarkedTabs)
280+
recentlySavedBookmarks.addAll(bookmarkTabs(tabIds))
281+
command.value = ShowUndoBookmarkMessage(recentlySavedBookmarks.size)
281282
}
282283
}
283284

284-
private suspend fun bookmarkTabs(tabIds: List<String>): Int {
285+
private suspend fun bookmarkTabs(tabIds: List<String>): List<Bookmark> {
285286
val results = tabIds.map { tabId ->
286287
viewModelScope.async {
287288
saveSiteBookmark(tabId)
288289
}
289290
}
290-
return results.awaitAll().count { it != null }
291+
return results.awaitAll().filterNotNull()
292+
}
293+
294+
fun onSelectionModeRequested() {
295+
triggerEmptySelectionMode()
296+
}
297+
298+
private fun triggerEmptySelectionMode() {
299+
_selectionViewState.update { it.copy(mode = Selection(emptyList())) }
300+
}
301+
302+
fun onCloseSelectedTabs() {
303+
}
304+
305+
fun onCloseOtherTabs() {
291306
}
292307

293308
fun onCloseAllTabsConfirmed() {
@@ -392,7 +407,7 @@ class TabSwitcherViewModel @Inject constructor(
392407

393408
private suspend fun saveSiteBookmark(tabId: String) = withContext(dispatcherProvider.io()) {
394409
var bookmark: Bookmark? = null
395-
(tabSwitcherItems.value?.firstOrNull { it.id == tabId } as? Tab)?.let { tab ->
410+
(tabItems.firstOrNull { it.id == tabId } as? Tab)?.let { tab ->
396411
tab.tabEntity.url?.let { url ->
397412
if (url.isNotBlank()) {
398413
// Only bookmark new sites

app/src/main/res/values/donottranslate.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,9 @@
9797
<item quantity="one">Bookmark Selected Tab?</item>
9898
<item quantity="other">Bookmark %1$d Tabs?</item>
9999
</plurals>
100-
<string name="tabSwitcherBookmarkToastZero">No unique bookmark was added.</string>
101100
<plurals name="tabSwitcherBookmarkToast" tools:ignore="ImpliedQuantity,MissingInstruction">
102-
<item quantity="one">One new bookmark was added.</item>
103-
<item quantity="other">%1$d new bookmarks were added.</item>
101+
<item quantity="one">%1$d bookmark added</item>
102+
<item quantity="other">%1$d bookmarks added</item>
104103
</plurals>
105104
<string name="tabSwitcherBookmarkDialogDescription">Existing bookmarks will not be duplicated.</string>
106105
<string name="tabSwitcherBookmarkDialogPositiveButton">Bookmark</string>

0 commit comments

Comments
 (0)