Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add toolbar icon for AI Chat #3421

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
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
20 changes: 20 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@
316913272BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */; };
316913292BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; };
3169132A2BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */; };
316C48EF2CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316C48EE2CC2B231000B08C1 /* AIChatPreferencesStorage.swift */; };
316C48F02CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316C48EE2CC2B231000B08C1 /* AIChatPreferencesStorage.swift */; };
3171D6B82889849F0068632A /* CookieManagedNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */; };
3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6B9288984D00068632A /* BadgeAnimationView.swift */; };
3171D6DB2889B64D0068632A /* CookieManagedNotificationContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */; };
Expand Down Expand Up @@ -311,6 +313,8 @@
31EF1E812B63FFB800E6DB17 /* DataBrokerProtectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3139A1512AA4B3C000969C7D /* DataBrokerProtectionManager.swift */; };
31EF1E832B63FFCA00E6DB17 /* LoginItem+DataBrokerProtection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D8FA00B2AC5BDCE005DD0D0 /* LoginItem+DataBrokerProtection.swift */; };
31EF1E842B63FFD100E6DB17 /* DataBrokerProtectionDebugMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316850712AF3AD58009A2828 /* DataBrokerProtectionDebugMenu.swift */; };
31F25EFF2CC3CA02002F9084 /* AIChatMenuConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F25EFE2CC3CA00002F9084 /* AIChatMenuConfigurationTests.swift */; };
31F25F002CC3CA02002F9084 /* AIChatMenuConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F25EFE2CC3CA00002F9084 /* AIChatMenuConfigurationTests.swift */; };
31F28C4F28C8EEC500119F70 /* YoutubePlayerUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */; };
31F28C5128C8EEC500119F70 /* YoutubeOverlayUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C4E28C8EEC500119F70 /* YoutubeOverlayUserScript.swift */; };
31F28C5328C8EECA00119F70 /* DuckURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */; };
Expand Down Expand Up @@ -3322,6 +3326,7 @@
316913222BD2B6250051B46D /* DataBrokerProtectionPixelsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionPixelsHandler.swift; sourceTree = "<group>"; };
316913252BD2B76F0051B46D /* DataBrokerPrerequisitesStatusVerifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerPrerequisitesStatusVerifier.swift; sourceTree = "<group>"; };
316913282BD2C7570051B46D /* DataBrokerProtectionErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionErrorViewController.swift; sourceTree = "<group>"; };
316C48EE2CC2B231000B08C1 /* AIChatPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatPreferencesStorage.swift; sourceTree = "<group>"; };
3171D6B72889849F0068632A /* CookieManagedNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationView.swift; sourceTree = "<group>"; };
3171D6B9288984D00068632A /* BadgeAnimationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeAnimationView.swift; sourceTree = "<group>"; };
3171D6DA2889B64D0068632A /* CookieManagedNotificationContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManagedNotificationContainerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3358,6 +3363,7 @@
31E163BC293A579E00963C10 /* PrivacyReferenceTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyReferenceTestHelper.swift; sourceTree = "<group>"; };
31E163BF293A581900963C10 /* privacy-reference-tests */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = "privacy-reference-tests"; path = "Submodules/privacy-reference-tests"; sourceTree = SOURCE_ROOT; };
31ECDA102BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerAuthenticationManagerBuilder.swift; sourceTree = "<group>"; };
31F25EFE2CC3CA00002F9084 /* AIChatMenuConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatMenuConfigurationTests.swift; sourceTree = "<group>"; };
31F28C4C28C8EEC500119F70 /* YoutubePlayerUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubePlayerUserScript.swift; sourceTree = "<group>"; };
31F28C4E28C8EEC500119F70 /* YoutubeOverlayUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = YoutubeOverlayUserScript.swift; sourceTree = "<group>"; };
31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckURLSchemeHandler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5196,6 +5202,7 @@
31521ABE2CC0139C00248E6F /* AIChat */ = {
isa = PBXGroup;
children = (
316C48EE2CC2B231000B08C1 /* AIChatPreferencesStorage.swift */,
31521AC22CC01BC300248E6F /* AIChatTabOpener.swift */,
31521ABF2CC013A400248E6F /* AIChatMenuVisibilityConfigurable.swift */,
);
Expand Down Expand Up @@ -5307,6 +5314,14 @@
path = Resources;
sourceTree = "<group>";
};
31F25EFD2CC3C9F9002F9084 /* AIChat */ = {
isa = PBXGroup;
children = (
31F25EFE2CC3CA00002F9084 /* AIChatMenuConfigurationTests.swift */,
);
path = AIChat;
sourceTree = "<group>";
};
31F28C4B28C8EE9000119F70 /* YoutubePlayer */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -7499,6 +7514,7 @@
AA585D93248FD31400E9A3E2 /* UnitTests */ = {
isa = PBXGroup;
children = (
31F25EFD2CC3C9F9002F9084 /* AIChat */,
56A054392C20876F007D8FAB /* DuckSchemeHandler */,
C13909F22B85FD60001626ED /* Autofill */,
5629846D2AC460DF00AC20EB /* Sync */,
Expand Down Expand Up @@ -10879,6 +10895,7 @@
3706FB56293F65D500E42796 /* NSSavePanelExtension.swift in Sources */,
B6B5F5802B024105008DB58A /* DataImportSummaryView.swift in Sources */,
4B9DB0422A983B24000927DB /* WaitlistDialogView.swift in Sources */,
316C48EF2CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */,
3706FB57293F65D500E42796 /* AppPrivacyConfigurationDataProvider.swift in Sources */,
3199AF7A2C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */,
C1B1CBE22BE1915100B6049C /* DataImportShortcutsViewModel.swift in Sources */,
Expand Down Expand Up @@ -11703,6 +11720,7 @@
56AC09C82C2D7DD6002D70E0 /* BookmarksBarViewControllerTests.swift in Sources */,
3706FE5B293F661700E42796 /* FirefoxLoginReaderTests.swift in Sources */,
1D1C36E429FAE8DA001FA40C /* FaviconManagerTests.swift in Sources */,
31F25EFF2CC3CA02002F9084 /* AIChatMenuConfigurationTests.swift in Sources */,
37716D8029707E5D00A9FC6D /* FireproofingReferenceTests.swift in Sources */,
561D29CB2BDA7530007B91D0 /* MockAppearancePreferencesPersistor.swift in Sources */,
B6AA64742994B43300D99CD6 /* FutureExtensionTests.swift in Sources */,
Expand Down Expand Up @@ -12584,6 +12602,7 @@
4BE65476271FCD41008D1D63 /* PasswordManagementCreditCardItemView.swift in Sources */,
AA5C8F59258FE21F00748EB7 /* NSTextFieldExtension.swift in Sources */,
3706FEC8293F6F7500E42796 /* BWManagement.swift in Sources */,
316C48F02CC2B232000B08C1 /* AIChatPreferencesStorage.swift in Sources */,
B6830961274CDE99004B46BB /* FireproofDomainsContainer.swift in Sources */,
B687B7CC2947A1E9001DEA6F /* ExternalAppSchemeHandler.swift in Sources */,
B65536AE2685E17200085A79 /* GeolocationService.swift in Sources */,
Expand Down Expand Up @@ -13266,6 +13285,7 @@
4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */,
F1AFDBD22C231B7A00710F2C /* SubscriptionAppStoreRestorerTests.swift in Sources */,
B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */,
31F25F002CC3CA02002F9084 /* AIChatMenuConfigurationTests.swift in Sources */,
AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */,
37D046A42C7DAA8900AEAA50 /* ImageProcessorMock.swift in Sources */,
1DFAB5222A8983DE00A0F7F6 /* SetExtensionTests.swift in Sources */,
Expand Down
53 changes: 34 additions & 19 deletions DuckDuckGo/AIChat/AIChatMenuVisibilityConfigurable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,71 @@ import Combine
protocol AIChatMenuVisibilityConfigurable {
var shouldDisplayApplicationMenuShortcut: Bool { get }
var shouldDisplayToolbarShortcut: Bool { get }

var isFeatureEnabledForApplicationMenuShortcut: Bool { get }
var isFeatureEnabledForToolbarShortcut: Bool { get }

var shortcutURL: URL { get }
var valuesChangedPublisher: PassthroughSubject<Void, Never> { get }
}

final class AIChatMenuConfiguration: AIChatMenuVisibilityConfigurable {
var valuesChangedPublisher = PassthroughSubject<Void, Never>()
private var cancellables = Set<AnyCancellable>()
private let preferences: AIChatPreferences

enum ShortcutType {
case applicationMenu
case toolbar
}

// MARK: - Public
private var cancellables = Set<AnyCancellable>()
private var storage: AIChatPreferencesStorage

var shouldDisplayApplicationMenuShortcut: Bool {
return isFeatureEnabledFor(shortcutType: .applicationMenu) && preferences.showShortcutInApplicationMenu
var valuesChangedPublisher = PassthroughSubject<Void, Never>()

var isFeatureEnabledForApplicationMenuShortcut: Bool {
isFeatureEnabledFor(shortcutType: .applicationMenu)
}

var isFeatureEnabledForToolbarShortcut: Bool {
isFeatureEnabledFor(shortcutType: .toolbar)
}

var shouldDisplayToolbarShortcut: Bool {
return isFeatureEnabledFor(shortcutType: .toolbar) && preferences.showShortcutInToolbar
return isFeatureEnabledForToolbarShortcut && storage.shouldDisplayToolbarShortcut
}

var shouldDisplayApplicationMenuShortcut: Bool {
return isFeatureEnabledForApplicationMenuShortcut && storage.showShortcutInApplicationMenu
}

var shortcutURL: URL {
URL(string: "https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=2")!
}

init(preferences: AIChatPreferences = .shared) {
self.preferences = preferences
subscribeToValueChanges()
init(storage: AIChatPreferencesStorage = DefaultAIChatPreferencesStorage()) {
self.storage = storage
self.subscribeToValuesChanged()
}

// MARK: - Private
private func subscribeToValuesChanged() {
storage.shouldDisplayToolbarShortcutPublisher
.removeDuplicates()
.sink { [weak self] _ in
self?.valuesChangedPublisher.send()
}.store(in: &cancellables)

private func subscribeToValueChanges() {
preferences.$showShortcutInToolbar
.merge(with: preferences.$showShortcutInApplicationMenu)
.dropFirst()
storage.showShortcutInApplicationMenuPublisher
.removeDuplicates()
.sink { [weak self] _ in
self?.valuesChangedPublisher.send(())
}
.store(in: &cancellables)
self?.valuesChangedPublisher.send()
}.store(in: &cancellables)
}

private func isFeatureEnabledFor(shortcutType: ShortcutType) -> Bool {
switch shortcutType {
case .applicationMenu:
// Use privacy config here
return true
case .toolbar:
// Use privacy config here
return true
}
}
Expand Down
101 changes: 101 additions & 0 deletions DuckDuckGo/AIChat/AIChatPreferencesStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// AIChatPreferencesStorage.swift
//
// Copyright © 2024 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 Combine

protocol AIChatPreferencesStorage {
var showShortcutInApplicationMenu: Bool { get set }
var shouldDisplayToolbarShortcut: Bool { get set }

var showShortcutInApplicationMenuPublisher: AnyPublisher<Bool, Never> { get }
var shouldDisplayToolbarShortcutPublisher: AnyPublisher<Bool, Never> { get }
}

struct DefaultAIChatPreferencesStorage: AIChatPreferencesStorage {
var showShortcutInApplicationMenuPublisher: AnyPublisher<Bool, Never> {
userDefaults.showAIChatShortcutInApplicationMenuPublisher
}

var shouldDisplayToolbarShortcutPublisher: AnyPublisher<Bool, Never> {
NotificationCenter.default.publisher(for: .PinnedViewsChanged)
.compactMap { notification -> PinnableView? in
guard let userInfo = notification.userInfo as? [String: Any],
let viewType = userInfo[LocalPinningManager.pinnedViewChangedNotificationViewTypeKey] as? String,
let view = PinnableView(rawValue: viewType) else {
return nil
}
return view == .aiChat ? view : nil
}
.flatMap { view -> AnyPublisher<Bool, Never> in
return Just(self.pinningManager.isPinned(view)).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}

private let userDefaults: UserDefaults
private let pinningManager: PinningManager

init(userDefaults: UserDefaults = .standard,
pinningManager: PinningManager = LocalPinningManager.shared) {
self.userDefaults = userDefaults
self.pinningManager = pinningManager
}

var shouldDisplayToolbarShortcut: Bool {
get { pinningManager.isPinned(.aiChat) }
set {
if newValue {
pinningManager.pin(.aiChat)
} else {
pinningManager.unpin(.aiChat)
}
}
}

var showShortcutInApplicationMenu: Bool {
get { userDefaults.showAIChatShortcutInApplicationMenu }
set { userDefaults.showAIChatShortcutInApplicationMenu = newValue }
}
}

private extension UserDefaults {
private var showAIChatShortcutInApplicationMenuKey: String {
"aichat.showAIChatShortcutInApplicationMenu"
}

static let showAIChatShortcutInApplicationMenuDefaultValue = false

@objc
dynamic var showAIChatShortcutInApplicationMenu: Bool {
get {
value(forKey: showAIChatShortcutInApplicationMenuKey) as? Bool ?? Self.showAIChatShortcutInApplicationMenuDefaultValue
}

set {
guard newValue != showAIChatShortcutInApplicationMenu else {
return
}

set(newValue, forKey: showAIChatShortcutInApplicationMenuKey)
}
}

var showAIChatShortcutInApplicationMenuPublisher: AnyPublisher<Bool, Never> {
publisher(for: \.showAIChatShortcutInApplicationMenu).eraseToAnyPublisher()
}
}
3 changes: 3 additions & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,9 @@ struct UserText {
static let bitwardenCommunicationInfo = NSLocalizedString("bitwarden.connect.communication-info", value: "All communication between Bitwarden and DuckDuckGo is encrypted and the data never leaves your device.", comment: "Warns users that all communication between the DuckDuckGo browser and the password manager Bitwarden is encrypted and doesn't leave the user device")
static let bitwardenHistoryInfo = NSLocalizedString("bitwarden.connect.history-info", value: "Bitwarden will have access to your browsing history.", comment: "Warn users that the password Manager Bitwarden will have access to their browsing history")

static let showAIChatShortcut = NSLocalizedString("pinning.show-aichat-shortcut", value: "Show AI Chat Shortcut", comment: "Menu item for showing the AI Chat shortcut")
static let hideAIChatShortcut = NSLocalizedString("pinning.hide-aichat-shortcut", value: "Hide AI Chat Shortcut", comment: "Menu item for hiding the AI Chat shortcut")

static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Passwords Shortcut", comment: "Menu item for showing the passwords shortcut")
static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Passwords Shortcut", comment: "Menu item for hiding the passwords shortcut")

Expand Down
12 changes: 10 additions & 2 deletions DuckDuckGo/MainWindow/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ final class MainViewController: NSViewController {
private var tabViewModelCancellables = Set<AnyCancellable>()
private var bookmarksBarVisibilityChangedCancellable: AnyCancellable?
private var eventMonitorCancellables = Set<AnyCancellable>()
private let aiChatMenuConfig: AIChatMenuVisibilityConfigurable

private var bookmarksBarIsVisible: Bool {
return bookmarksBarViewController.parent != nil
Expand All @@ -59,8 +60,10 @@ final class MainViewController: NSViewController {
init(tabCollectionViewModel: TabCollectionViewModel? = nil,
bookmarkManager: BookmarkManager = LocalBookmarkManager.shared,
autofillPopoverPresenter: AutofillPopoverPresenter,
vpnXPCClient: VPNControllerXPCClient = .shared) {
vpnXPCClient: VPNControllerXPCClient = .shared,
aiChatMenuConfig: AIChatMenuVisibilityConfigurable = AIChatMenuConfiguration()) {

self.aiChatMenuConfig = aiChatMenuConfig
let tabCollectionViewModel = tabCollectionViewModel ?? TabCollectionViewModel()
self.tabCollectionViewModel = tabCollectionViewModel
self.isBurner = tabCollectionViewModel.isBurner
Expand Down Expand Up @@ -108,7 +111,12 @@ final class MainViewController: NSViewController {
)
}()

navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, isBurner: isBurner, networkProtectionPopoverManager: networkProtectionPopoverManager, networkProtectionStatusReporter: networkProtectionStatusReporter, autofillPopoverPresenter: autofillPopoverPresenter)
navigationBarViewController = NavigationBarViewController.create(tabCollectionViewModel: tabCollectionViewModel,
isBurner: isBurner,
networkProtectionPopoverManager: networkProtectionPopoverManager,
networkProtectionStatusReporter: networkProtectionStatusReporter,
autofillPopoverPresenter: autofillPopoverPresenter,
aiChatMenuConfig: aiChatMenuConfig)

browserTabViewController = BrowserTabViewController(tabCollectionViewModel: tabCollectionViewModel, bookmarkManager: bookmarkManager)
findInPageViewController = FindInPageViewController.create()
Expand Down
Loading
Loading