Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.bookmarks.dialog

import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.LayoutInflater
import android.view.WindowManager
import android.widget.FrameLayout
import androidx.lifecycle.lifecycleScope
import com.duckduckgo.app.browser.databinding.BottomSheetAddBookmarkBinding
import com.duckduckgo.common.utils.ConflatedJob
import com.duckduckgo.savedsites.api.models.BookmarkFolder
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.ShapeAppearanceModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import com.duckduckgo.mobile.android.R as CommonR

@SuppressLint("NoBottomSheetDialog")
class BookmarkAddedConfirmationDialog(
context: Context,
private val bookmarkFolder: BookmarkFolder?,
) : BottomSheetDialog(context) {

abstract class EventListener {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why not an interface?

Copy link
Member Author

@CDRussell CDRussell Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would normally go for interface personally, but this was porting over existing code (renaming the dialog) so wanted to minimize changes made. It also seems to be fairly consistent with other dialogs/builders, so I just kept it as it was.

Screenshot 2025-11-19 at 11 13 32

/** Sets a listener to be invoked when favorite state is changed */
open fun onFavoriteStateChangeClicked(isFavorited: Boolean) {}

/** Sets a listener to be invoked when edit bookmarks is clicked */
open fun onEditBookmarkClicked() {}
}

private var listener: EventListener? = null

private val binding = BottomSheetAddBookmarkBinding.inflate(LayoutInflater.from(context))

private val autoDismissDialogJob = ConflatedJob()

override fun show() {
setContentView(binding.root)

window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.isDraggable = false
roundCornersAlways(this)
binding.bookmarksBottomSheetDialogTitle.text = getBookmarksBottomSheetTitle(context, bookmarkFolder)

binding.setAsFavorite.setOnClickListener {
cancelDialogAutoDismiss()
binding.setAsFavoriteSwitch.isChecked = !binding.setAsFavoriteSwitch.isChecked
listener?.onFavoriteStateChangeClicked(binding.setAsFavoriteSwitch.isChecked)
}
binding.setAsFavoriteSwitch.setOnClickListener {
cancelDialogAutoDismiss()
listener?.onFavoriteStateChangeClicked(binding.setAsFavoriteSwitch.isChecked)
}
binding.editBookmark.setOnClickListener {
cancelDialogAutoDismiss()
listener?.onEditBookmarkClicked()
dismiss()
}

autoDismissDialogJob += lifecycleScope.launch {
delay(BOOKMARKS_BOTTOM_SHEET_DURATION)
dismiss()
}
super.show()
}

private fun cancelDialogAutoDismiss() {
autoDismissDialogJob.cancel()
}

private fun getBookmarksBottomSheetTitle(context: Context, bookmarkFolder: BookmarkFolder?): SpannableString {
val folderName = bookmarkFolder?.name ?: ""
val fullText = context.getString(com.duckduckgo.saved.sites.impl.R.string.bookmarkAddedInBookmarks, folderName)
val spannableString = SpannableString(fullText)

val boldStart = fullText.indexOf(folderName)
val boldEnd = boldStart + folderName.length
spannableString.setSpan(StyleSpan(Typeface.BOLD), boldStart, boldEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannableString
}

/** Sets event listener for the bottom sheet dialog */
fun addEventListener(eventListener: EventListener) {
listener = eventListener
}

// TODO: Use a style when bookmarks is moved to its own module
private fun roundCornersAlways(dialog: BottomSheetDialog) {
dialog.setOnShowListener { dialogInterface ->
val bottomSheetDialog = dialogInterface as BottomSheetDialog
val bottomSheet = bottomSheetDialog.findViewById<FrameLayout>(R.id.design_bottom_sheet)
bottomSheet?.background = MaterialShapeDrawable(
ShapeAppearanceModel.builder().apply {
setTopLeftCorner(CornerFamily.ROUNDED, context.resources.getDimension(CommonR.dimen.dialogBorderRadius))
setTopRightCorner(CornerFamily.ROUNDED, context.resources.getDimension(CommonR.dimen.dialogBorderRadius))
}.build(),
)
}
}

private companion object {
private const val BOOKMARKS_BOTTOM_SHEET_DURATION = 3_500L
}
}
93 changes: 21 additions & 72 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,16 @@ import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.content.res.Configuration
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.print.PrintAttributes
import android.print.PrintManager
import android.provider.MediaStore
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned
import android.text.style.StyleSpan
import android.view.ContextMenu
import android.view.MenuItem
import android.view.MotionEvent
Expand Down Expand Up @@ -102,6 +97,7 @@ import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.accessibility.data.AccessibilitySettingsDataStore
import com.duckduckgo.app.bookmarks.dialog.BookmarkAddedConfirmationDialog
import com.duckduckgo.app.browser.BrowserTabViewModel.FileChooserRequestedParams
import com.duckduckgo.app.browser.R.string
import com.duckduckgo.app.browser.SSLErrorType.NONE
Expand Down Expand Up @@ -322,11 +318,9 @@ import com.duckduckgo.privacy.dashboard.api.ui.PrivacyDashboardHybridScreenResul
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopup
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupFactory
import com.duckduckgo.privacyprotectionspopup.api.PrivacyProtectionsPopupViewState
import com.duckduckgo.savedsites.api.models.BookmarkFolder
import com.duckduckgo.savedsites.api.models.SavedSite
import com.duckduckgo.savedsites.api.models.SavedSite.Bookmark
import com.duckduckgo.savedsites.api.models.SavedSitesNames
import com.duckduckgo.savedsites.impl.bookmarks.BookmarksBottomSheetDialog
import com.duckduckgo.savedsites.impl.bookmarks.FaviconPromptSheet
import com.duckduckgo.savedsites.impl.dialogs.EditSavedSiteDialogFragment
import com.duckduckgo.serp.logos.api.SerpLogoScreens.EasterEggLogoScreen
Expand Down Expand Up @@ -651,8 +645,6 @@ class BrowserTabFragment :

private lateinit var webViewContainer: FrameLayout

private var bookmarksBottomSheetDialog: BookmarksBottomSheetDialog.Builder? = null

private var autocompleteItemOffsetTop: Int = 0
private var autocompleteFirstVisibleItemPosition: Int = 0

Expand Down Expand Up @@ -3770,70 +3762,30 @@ class BrowserTabFragment :
}

private fun savedSiteAdded(savedSiteChangedViewState: SavedSiteChangedViewState) {
val dismissHandler = Handler(Looper.getMainLooper())
val dismissRunnable =
Runnable {
if (isAdded) {
bookmarksBottomSheetDialog?.dialog?.let { dialog ->
if (dialog.isShowing) {
dialog.dismiss()
}
context?.let { ctx ->
val dialog = BookmarkAddedConfirmationDialog(ctx, savedSiteChangedViewState.bookmarkFolder)
dialog.addEventListener(
object : BookmarkAddedConfirmationDialog.EventListener() {
override fun onFavoriteStateChangeClicked(isFavorited: Boolean) {
viewModel.onFavoriteMenuClicked()
}
}
}
val title = getBookmarksBottomSheetTitle(savedSiteChangedViewState.bookmarkFolder)

bookmarksBottomSheetDialog =
BookmarksBottomSheetDialog
.Builder(requireContext())
.setTitle(title)
.setPrimaryItem(
getString(com.duckduckgo.saved.sites.impl.R.string.addToFavorites),
icon = com.duckduckgo.mobile.android.R.drawable.ic_favorite_24,
).setSecondaryItem(
getString(com.duckduckgo.saved.sites.impl.R.string.editBookmark),
icon = com.duckduckgo.mobile.android.R.drawable.ic_edit_24,
).addEventListener(
object : BookmarksBottomSheetDialog.EventListener() {
override fun onPrimaryItemClicked() {
viewModel.onFavoriteMenuClicked()
dismissHandler.removeCallbacks(dismissRunnable)
}

override fun onSecondaryItemClicked() {
if (savedSiteChangedViewState.savedSite is Bookmark) {
pixel.fire(AppPixelName.ADD_BOOKMARK_CONFIRM_EDITED)
editSavedSite(
savedSiteChangedViewState.copy(
savedSite = savedSiteChangedViewState.savedSite.copy(
isFavorite = viewModel.browserViewState.value?.favorite != null,
),
override fun onEditBookmarkClicked() {
if (savedSiteChangedViewState.savedSite is Bookmark) {
pixel.fire(AppPixelName.ADD_BOOKMARK_CONFIRM_EDITED)
editSavedSite(
savedSiteChangedViewState.copy(
savedSite = savedSiteChangedViewState.savedSite.copy(
isFavorite = viewModel.browserViewState.value?.favorite != null,
),
)
dismissHandler.removeCallbacks(dismissRunnable)
}
}

override fun onBottomSheetDismissed() {
super.onBottomSheetDismissed()
dismissHandler.removeCallbacks(dismissRunnable)
),
)
}
},
)
bookmarksBottomSheetDialog?.show()

dismissHandler.postDelayed(dismissRunnable, BOOKMARKS_BOTTOM_SHEET_DURATION)
}

private fun getBookmarksBottomSheetTitle(bookmarkFolder: BookmarkFolder?): SpannableString {
val folderName = bookmarkFolder?.name ?: ""
val fullText = getString(com.duckduckgo.saved.sites.impl.R.string.bookmarkAddedInBookmarks, folderName)
val spannableString = SpannableString(fullText)

val boldStart = fullText.indexOf(folderName)
val boldEnd = boldStart + folderName.length
spannableString.setSpan(StyleSpan(Typeface.BOLD), boldStart, boldEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannableString
}
},
)
dialog.show()
}
}

private fun editSavedSite(savedSiteChangedViewState: SavedSiteChangedViewState) {
Expand Down Expand Up @@ -4371,8 +4323,6 @@ class BrowserTabFragment :

private const val COOKIES_ANIMATION_DELAY = 400L

private const val BOOKMARKS_BOTTOM_SHEET_DURATION = 3500L

private const val AUTOCOMPLETE_PADDING_DP = 6

private const val SITE_SECURITY_WARNING = "Warning: Security Risk"
Expand Down Expand Up @@ -4618,7 +4568,6 @@ class BrowserTabFragment :
renderFullscreenMode(viewState)
privacyProtectionsPopup.setViewState(viewState.privacyProtectionsPopupViewState)

bookmarksBottomSheetDialog?.dialog?.toggleSwitch(viewState.favorite != null)
val bookmark =
viewModel.browserViewState.value
?.bookmark
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/rounded_top_corners_bottom_sheet_drawable"
android:orientation="vertical"
android:paddingTop="@dimen/actionBottomSheetVerticalPadding"
android:paddingBottom="@dimen/actionBottomSheetVerticalPadding"
android:background="@drawable/rounded_top_corners_bottom_sheet_drawable">
android:paddingBottom="@dimen/actionBottomSheetVerticalPadding">

<com.duckduckgo.common.ui.view.text.DaxTextView
android:id="@+id/bookmarksBottomSheetDialogTitle"
Expand All @@ -31,7 +31,6 @@
android:layout_marginStart="@dimen/keyline_4"
android:paddingTop="@dimen/bottomSheetTitleVerticalPadding"
android:paddingBottom="@dimen/bottomSheetTitleVerticalPadding"
android:visibility="gone"
app:textType="secondary"
app:typography="body1"
tools:text="Actions" />
Expand All @@ -41,14 +40,15 @@
android:layout_height="wrap_content">

<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/bookmarksBottomSheetDialogPrimaryItem"
android:id="@+id/setAsFavorite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:leadingIconBackground="circular"
app:primaryText="Primary Item" />
app:primaryText="@string/addToFavorites"
app:leadingIcon="@drawable/ic_favorite_24" />

<com.duckduckgo.common.ui.view.DaxSwitch
android:id="@+id/bookmarksBottomSheetSwitch"
android:id="@+id/setAsFavoriteSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
Expand All @@ -57,10 +57,11 @@


<com.duckduckgo.common.ui.view.listitem.OneLineListItem
android:id="@+id/bookmarksBottomSheetDialogSecondaryItem"
android:id="@+id/editBookmark"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:leadingIcon="@drawable/ic_edit_24"
app:leadingIconBackground="circular"
app:primaryText="Secondary Item" />
app:primaryText="@string/editBookmark" />

</LinearLayout>
Loading
Loading