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
12 changes: 12 additions & 0 deletions iOS/DuckDuckGo-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -452,9 +452,12 @@
6F5041C92CC11A5100989E48 /* NewTabPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5041C82CC11A5100989E48 /* NewTabPageView.swift */; };
6F55AE5A2ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE592ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift */; };
6F55AE612ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE5F2ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift */; };
6F55AE622ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE602ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift */; };
6F55AE642ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55AE632ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift */; };
6F55BCF92ECE29B3009E50C1 /* BrowsingMenuSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F55BCF82ECE29B3009E50C1 /* BrowsingMenuSheetView.swift */; };
6F5938982DB1028200C8C068 /* BrowserChromeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5938972DB1028200C8C068 /* BrowserChromeButton.swift */; };
6F5AA3EF2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */; };
6F5B2FD92EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5B2FD82EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift */; };
6F5DCFCE2E854C2000758C8A /* UniversalOmniBarEditingStateTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F5DCFCD2E854C2000758C8A /* UniversalOmniBarEditingStateTransition.swift */; };
6F64AA532C47E92600CF4489 /* FavoritesFaviconLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */; };
6F655BE22BAB289E00AC3597 /* DefaultTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */; };
Expand Down Expand Up @@ -2575,9 +2578,12 @@
6F5041C82CC11A5100989E48 /* NewTabPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageView.swift; sourceTree = "<group>"; };
6F55AE592ED8E2E8005D1F5B /* BrowsingMenuVariantBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantBuilder.swift; sourceTree = "<group>"; };
6F55AE5F2ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantABuilder.swift; sourceTree = "<group>"; };
6F55AE602ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantBBuilder.swift; sourceTree = "<group>"; };
6F55AE632ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantCBuilder.swift; sourceTree = "<group>"; };
6F55BCF82ECE29B3009E50C1 /* BrowsingMenuSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuSheetView.swift; sourceTree = "<group>"; };
6F5938972DB1028200C8C068 /* BrowserChromeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserChromeButton.swift; sourceTree = "<group>"; };
6F5AA3EE2CC1588400685CB4 /* FavoritesListInteractingAdapterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesListInteractingAdapterTests.swift; sourceTree = "<group>"; };
6F5B2FD82EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowsingMenuVariantDBuilder.swift; sourceTree = "<group>"; };
6F5DCFCD2E854C2000758C8A /* UniversalOmniBarEditingStateTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalOmniBarEditingStateTransition.swift; sourceTree = "<group>"; };
6F64AA522C47E92600CF4489 /* FavoritesFaviconLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesFaviconLoader.swift; sourceTree = "<group>"; };
6F655BE12BAB289E00AC3597 /* DefaultTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultTheme.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5871,6 +5877,9 @@
6F55BCF82ECE29B3009E50C1 /* BrowsingMenuSheetView.swift */,
6F6DACE22ED70B5200DB841C /* BrowsingMenuSheetCapability.swift */,
6F55AE5F2ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift */,
6F55AE602ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift */,
6F55AE632ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift */,
6F5B2FD82EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift */,
);
path = SheetPresentationMenu;
sourceTree = "<group>";
Expand Down Expand Up @@ -10894,6 +10903,7 @@
3157B43527F497F50042D3D7 /* SaveLoginViewController.swift in Sources */,
317A247F2D8336650033A0D6 /* DownloadsDirectoryHandling.swift in Sources */,
853807922D6E638000CE1455 /* AlertPlaygroundView.swift in Sources */,
6F55AE642ED8EBE6005D1F5B /* BrowsingMenuVariantCBuilder.swift in Sources */,
C14D43012B45D6CD00ACA4DC /* AutofillDebugViewController.swift in Sources */,
856F28D72D3EC5FA00A88211 /* TabSwitcherViewController+MultiSelect.swift in Sources */,
853C5F6121C277C7001F7A05 /* global.swift in Sources */,
Expand Down Expand Up @@ -11087,6 +11097,7 @@
C128BA252DF0847900ADDC5F /* SettingsCompleteSetupView.swift in Sources */,
C1C18BF62D956FAD00FFBA2E /* AutofillEditableCell.swift in Sources */,
6F55AE612ED8E7BB005D1F5B /* BrowsingMenuVariantABuilder.swift in Sources */,
6F55AE622ED8E7BB005D1F5B /* BrowsingMenuVariantBBuilder.swift in Sources */,
65BD16352D8C206F000795DE /* DuckPlayerPrimingModalView.swift in Sources */,
65BD16362D8C206F000795DE /* DuckPlayerPrimingModalViewModel.swift in Sources */,
C1641EAF2BC2F5140012607A /* ImportPasswordsViaSyncViewController.swift in Sources */,
Expand Down Expand Up @@ -11478,6 +11489,7 @@
CB7E0A232D5E1E55002A7C0C /* LaunchActionHandler.swift in Sources */,
97770B702EC1ED2600CACA68 /* OnboardingSearchExperiencePicker.swift in Sources */,
97770B712EC1ED2600CACA68 /* OnboardingSearchExperiencePickerViewModel.swift in Sources */,
6F5B2FD92EDA4CB100E85FAC /* BrowsingMenuVariantDBuilder.swift in Sources */,
97770B722EC1ED2600CACA68 /* OnboardingSearchExperienceProvider.swift in Sources */,
97770B732EC1ED2600CACA68 /* OnboardingSearchExperienceSelectionHandler.swift in Sources */,
C189966E2E4F45A200246F22 /* Logger+DaxEasterEgg.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,19 @@ enum BrowsingMenuClusteringVariant: String, CaseIterable, CustomStringConvertibl
switch self {
case .a:
"Production"
case .b:
"Easy Shortcuts"
case .c:
"Easy Privacy Tools"
case .d:
"Easy Privacy - No floating button"
}
}

case a
case b
case c
case d
}

enum BrowsingMenuSheetCapability {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,12 @@ struct BrowsingMenuSheetView: View {
private let onDismiss: () -> Void

@State private var highlightTag: BrowsingMenuModel.Entry.Tag?

@State private var actionToPerform: () -> Void
@State private var actionToPerform: () -> Void = {}

init(model: BrowsingMenuModel, highlightRowWithTag: BrowsingMenuModel.Entry.Tag? = nil, onDismiss: @escaping () -> Void) {
self.model = model
self.onDismiss = onDismiss
self.highlightTag = highlightRowWithTag
self.actionToPerform = { }
_highlightTag = State(initialValue: highlightRowWithTag)
}

var body: some View {
Expand Down Expand Up @@ -89,6 +87,12 @@ struct BrowsingMenuSheetView: View {
actionToPerform()
onDismiss()
})
.floatingToolbar(
footerItems: model.footerItems,
actionToPerform: $actionToPerform,
presentationMode: presentationMode,
showsLabels: model.footerItems.count < 2
)
}
.tint(Color(designSystemColor: .textPrimary))
.background((Color(designSystemColor: .background)))
Expand Down Expand Up @@ -213,3 +217,68 @@ private struct MenuHeaderButton: View {
static let cornerRadius: CGFloat = 4
}
}

private extension View {
func floatingToolbar(
footerItems: [BrowsingMenuModel.Entry],
actionToPerform: Binding<() -> Void>,
presentationMode: Binding<PresentationMode>,
showsLabels: Bool
) -> some View {
modifier(FloatingToolbarModifier(
footerItems: footerItems,
actionToPerform: actionToPerform,
presentationMode: presentationMode,
showsLabels: showsLabels
))
}
}

private struct FloatingToolbarModifier: ViewModifier {
let footerItems: [BrowsingMenuModel.Entry]
@Binding var actionToPerform: () -> Void
let presentationMode: Binding<PresentationMode>
let showsLabels: Bool

func body(content: Content) -> some View {
if footerItems.isEmpty {
content
} else {
content
.safeAreaInset(edge: .bottom, content: {
createBottomToolbar(labels: showsLabels)
})
}
}

@ViewBuilder
private func createBottomToolbar(labels: Bool = false) -> some View {
HStack(spacing: 4) {
ForEach(footerItems) { footerItem in
Button(action: {
actionToPerform = { footerItem.action() }
presentationMode.wrappedValue.dismiss()
}) {
HStack(spacing: 4) {
Image(uiImage: footerItem.image)
.tint(Color(designSystemColor: .icons))
if labels {
Text(footerItem.name)
.daxBodyRegular()
.foregroundStyle(Color(designSystemColor: .textPrimary))
}
}
.padding(.vertical, 8)
.padding(.horizontal, 16)
}
.buttonStyle(.plain)
.accessibilityLabel(footerItem.accessibilityLabel ?? footerItem.name)
}
}
.background(Color(designSystemColor: .surfaceCanvas))
.clipShape(RoundedRectangle(cornerRadius: 12))
.shadow(color: Color(designSystemColor: .shadowSecondary), radius: 4, x: 0, y: 4)
.shadow(color: Color(designSystemColor: .shadowSecondary), radius: 2, x: 0, y: 1)
.fixedSize(horizontal: true, vertical: true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// BrowsingMenuVariantBBuilder.swift
// DuckDuckGo
//
// Copyright © 2025 DuckDuckGo. All rights reserved.
//
// 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.
//

import Foundation
import Bookmarks
import Core

/// Variant B Builder: Custom structure with specific sections
final class BrowsingMenuVariantBBuilder: BrowsingMenuVariantBuilder {
weak var entryBuilder: BrowsingMenuEntryBuilding?

init(entryBuilder: BrowsingMenuEntryBuilding) {
self.entryBuilder = entryBuilder
}

func buildMenu(
context: BrowsingMenuContext,
bookmarksInterface: MenuBookmarksInteracting,
mobileCustomization: MobileCustomization,
clearTabsAndData: @escaping () -> Void
) -> BrowsingMenuModel? {

switch context {
case .newTabPage:
return buildNewTabPageMenu(mobileCustomization: mobileCustomization,
clearTabsAndData: clearTabsAndData)

case .aiChatTab:
return buildAIChatMenu()

case .website:
return buildWebsiteMenu(
bookmarksInterface: bookmarksInterface,
mobileCustomization: mobileCustomization,
clearTabsAndData: clearTabsAndData
)
}
}

private func buildNewTabPageMenu(mobileCustomization: MobileCustomization,
clearTabsAndData: @escaping () -> Void) -> BrowsingMenuModel? {
guard let entryBuilder = entryBuilder else { return nil }

let headerItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeNewTabEntry()),
.init(entryBuilder.makeChatEntry(withSmallIcon: false))
].compactMap { $0 }

let shortcutsItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeOpenBookmarksEntry()),
.init(entryBuilder.makeAutoFillEntry()),
.init(entryBuilder.makeDownloadsEntry())
].compactMap { $0 }

let privacyItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeVPNEntry()),
.init(entryBuilder.makeClearDataEntry(mobileCustomization: mobileCustomization, clearTabsAndData: clearTabsAndData))
].compactMap { $0 }

let sections = [
BrowsingMenuModel.Section(items: shortcutsItems),
BrowsingMenuModel.Section(items: privacyItems)
]

let footerItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeSettingsEntry(useSmallIcon: false))
].compactMap { $0 }

return BrowsingMenuModel(
headerItems: headerItems,
sections: sections,
footerItems: footerItems
)
}

private func buildWebsiteMenu(
bookmarksInterface: MenuBookmarksInteracting,
mobileCustomization: MobileCustomization,
clearTabsAndData: @escaping () -> Void
) -> BrowsingMenuModel? {
guard let entryBuilder = entryBuilder else { return nil }

// Header: new tab, duck.ai (conditional)
let headerItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeNewTabEntry()),
.init(entryBuilder.makeChatEntry(withSmallIcon: false))
].compactMap { $0 }

// Sections
var sections = [BrowsingMenuModel.Section]()

// Link section
if let bookmarkEntries = entryBuilder.makeBookmarkEntries(with: bookmarksInterface) {
let linkItems: [BrowsingMenuModel.Entry] = [
.init(bookmarkEntries.bookmark),
.init(bookmarkEntries.favorite, tag: .favorite),
.init(entryBuilder.makeShareEntry(useSmallIcon: true))
].compactMap { $0 }
sections.append(BrowsingMenuModel.Section(items: linkItems))
}

// Tab actions section
let tabActionItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeFindInPageEntry()),
.init(entryBuilder.makeZoomEntry()),
.init(entryBuilder.makeDesktopSiteEntry())
].compactMap { $0 }

if !tabActionItems.isEmpty {
sections.append(BrowsingMenuModel.Section(items: tabActionItems))
}

// Shortcuts section
let shortcutItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeOpenBookmarksEntry()),
.init(entryBuilder.makeAutoFillEntry()),
.init(entryBuilder.makeDownloadsEntry())
].compactMap { $0 }

if !shortcutItems.isEmpty {
sections.append(BrowsingMenuModel.Section(items: shortcutItems))
}

// Privacy section
let privacyItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeVPNEntry()),
.init(entryBuilder.makeUseNewDuckAddressEntry()),
.init(entryBuilder.makeToggleProtectionEntry()),
.init(entryBuilder.makeKeepSignInEntry()),
.init(entryBuilder.makeClearDataEntry(mobileCustomization: mobileCustomization, clearTabsAndData: clearTabsAndData))
].compactMap { $0 }

if !privacyItems.isEmpty {
sections.append(BrowsingMenuModel.Section(items: privacyItems))
}

// Other section
let otherItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeReloadEntry()),
.init(entryBuilder.makeReportBrokenSiteEntry()),
.init(entryBuilder.makePrintEntry(withSmallIcon: true))
].compactMap { $0 }

if !otherItems.isEmpty {
sections.append(BrowsingMenuModel.Section(items: otherItems))
}

// Footer
let footerItems: [BrowsingMenuModel.Entry] = [
.init(entryBuilder.makeSettingsEntry(useSmallIcon: false))
].compactMap { $0 }

return BrowsingMenuModel(
headerItems: headerItems,
sections: sections,
footerItems: footerItems
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ protocol BrowsingMenuEntryBuilding: AnyObject {
mobileCustomization: MobileCustomization,
clearTabsAndData: @escaping () -> Void) -> [BrowsingMenuEntry]
func makeBrowsingMenuHeaderContent() -> [BrowsingMenuEntry]

func makeNewTabEntry() -> BrowsingMenuEntry
func makeChatEntry(withSmallIcon: Bool) -> BrowsingMenuEntry?
func makeSettingsEntry(useSmallIcon: Bool) -> BrowsingMenuEntry
func makeShareEntry(useSmallIcon: Bool) -> BrowsingMenuEntry
func makePrintEntry(withSmallIcon: Bool) -> BrowsingMenuEntry
func makeDownloadsEntry() -> BrowsingMenuEntry
func makeAutoFillEntry() -> BrowsingMenuEntry?
func makeVPNEntry() -> BrowsingMenuEntry?
func makeOpenBookmarksEntry() -> BrowsingMenuEntry
func makeBookmarkEntries(with bookmarksInterface: MenuBookmarksInteracting) -> (bookmark: BrowsingMenuEntry, favorite: BrowsingMenuEntry)?
func makeFindInPageEntry() -> BrowsingMenuEntry?
func makeZoomEntry() -> BrowsingMenuEntry?
func makeDesktopSiteEntry() -> BrowsingMenuEntry?
func makeReloadEntry() -> BrowsingMenuEntry?
func makeToggleProtectionEntry() -> BrowsingMenuEntry?
func makeReportBrokenSiteEntry() -> BrowsingMenuEntry?
func makeClearDataEntry(mobileCustomization: MobileCustomization, clearTabsAndData: @escaping () -> Void) -> BrowsingMenuEntry?
func makeUseNewDuckAddressEntry() -> BrowsingMenuEntry?
func makeKeepSignInEntry() -> BrowsingMenuEntry?
}

protocol BrowsingMenuVariantBuilder: AnyObject {
Expand Down
Loading
Loading