diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 9aa0fa9757..8bf183dafd 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -57,9 +57,6 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/1208592102886666/1208613627589762/f case crashReportOptInStatusResetting - /// https://app.asana.com/0/1204167627774280/1208794395441049/f - case aiChatBrowsingToolbarShortcut - case isPrivacyProLaunchedROW case isPrivacyProLaunchedROWOverride @@ -140,8 +137,6 @@ extension FeatureFlag: FeatureFlagDescribing { return .remoteReleasable(.feature(.adAttributionReporting)) case .crashReportOptInStatusResetting: return .internalOnly() - case .aiChatBrowsingToolbarShortcut: - return .remoteReleasable(.subfeature(AIChatSubfeature.browsingToolbarShortcut)) case .isPrivacyProLaunchedROW: return .remoteReleasable(.subfeature(PrivacyProSubfeature.isLaunchedROW)) case .isPrivacyProLaunchedROWOverride: diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 90107bf941..229a66f4e7 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -164,9 +164,10 @@ 316790E52C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */; }; 316931D727BD10BB0095F5ED /* SaveToDownloadsAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */; }; 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */; }; - 316AA45A2CF8E31F00A2ED28 /* AIChatRemoteSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettings.swift */; }; + 316AA45A2CF8E31F00A2ED28 /* AIChatSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316AA4592CF8E31F00A2ED28 /* AIChatSettings.swift */; }; 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; }; 317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; }; + 317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */; }; 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; }; 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; }; 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; }; @@ -1527,10 +1528,11 @@ 316790E42C9352190090B0A2 /* MarketplaceAdPostbackManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackManagerTests.swift; sourceTree = ""; }; 316931D627BD10BB0095F5ED /* SaveToDownloadsAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToDownloadsAlert.swift; sourceTree = ""; }; 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadActionMessageViewHelper.swift; sourceTree = ""; }; - 316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatRemoteSettings.swift; sourceTree = ""; }; + 316AA4592CF8E31F00A2ED28 /* AIChatSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatSettings.swift; sourceTree = ""; }; 3170048127A9504F00C03F35 /* DownloadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMocks.swift; sourceTree = ""; }; 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = ""; }; 31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = ""; }; + 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAIChatView.swift; sourceTree = ""; }; 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = ""; }; 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = ""; }; 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = ""; }; @@ -3348,6 +3350,7 @@ 1DEAADEB2BA45B4400E25A97 /* SettingsAccessibilityView.swift */, 1DEAADED2BA45DFE00E25A97 /* SettingsDataClearingView.swift */, D65625A02C232F5E006EF297 /* SettingsDuckPlayerView.swift */, + 317CA3422CFF82DB00F88848 /* SettingsAIChatView.swift */, ); name = MainSettings; sourceTree = ""; @@ -3599,7 +3602,7 @@ isa = PBXGroup; children = ( 31043B152CFA5B890028A97F /* AIChatPixelHandler.swift */, - 316AA4592CF8E31F00A2ED28 /* AIChatRemoteSettings.swift */, + 316AA4592CF8E31F00A2ED28 /* AIChatSettings.swift */, ); path = AIChat; sourceTree = ""; @@ -7820,7 +7823,7 @@ D668D92B2B696840008E2FF2 /* IdentityTheftRestorationPagesFeature.swift in Sources */, 85F2FFCF2211F8E5006BB258 /* TabSwitcherViewController+KeyCommands.swift in Sources */, 3157B43327F497E90042D3D7 /* SaveLoginView.swift in Sources */, - 316AA45A2CF8E31F00A2ED28 /* AIChatRemoteSettings.swift in Sources */, + 316AA45A2CF8E31F00A2ED28 /* AIChatSettings.swift in Sources */, F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */, BDE91CDC2C62AA3A0005CB74 /* DefaultMetadataCollector.swift in Sources */, D664C7C82B289AA200CBFA76 /* SubscriptionFlowView.swift in Sources */, @@ -8132,6 +8135,7 @@ 8590CB67268A2E520089F6BF /* RootDebugViewController.swift in Sources */, 1DEAADEA2BA4539800E25A97 /* SettingsAppearanceView.swift in Sources */, B623C1C22862CA9E0043013E /* DownloadSession.swift in Sources */, + 317CA3432CFF82E100F88848 /* SettingsAIChatView.swift in Sources */, 9F7CFF7F2C8A94F70012833E /* OnboardingView+AddressBarPositionContent.swift in Sources */, 985892522260B1B200EEB31B /* ProgressView.swift in Sources */, 85BA585A1F3506AE00C6E8CA /* AppSettings.swift in Sources */, diff --git a/DuckDuckGo/AIChat/AIChatRemoteSettings.swift b/DuckDuckGo/AIChat/AIChatSettings.swift similarity index 50% rename from DuckDuckGo/AIChat/AIChatRemoteSettings.swift rename to DuckDuckGo/AIChat/AIChatSettings.swift index b069e729a2..6a0b1e3706 100644 --- a/DuckDuckGo/AIChat/AIChatRemoteSettings.swift +++ b/DuckDuckGo/AIChat/AIChatSettings.swift @@ -1,5 +1,5 @@ // -// AIChatRemoteSettings.swift +// AIChatSettings.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -24,7 +24,7 @@ import Core /// This struct serves as a wrapper for PrivacyConfigurationManaging, enabling the retrieval of data relevant to AIChat. /// It also fire pixels when necessary data is missing. -struct AIChatRemoteSettings: AIChatRemoteSettingsProvider { +struct AIChatSettings: AIChatSettingsProvider { enum SettingsValue: String { case aiChatURL @@ -36,12 +36,16 @@ struct AIChatRemoteSettings: AIChatRemoteSettingsProvider { } private let privacyConfigurationManager: PrivacyConfigurationManaging - private var settings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings { + private var remoteSettings: PrivacyConfigurationData.PrivacyFeature.FeatureSettings { privacyConfigurationManager.privacyConfig.settings(for: .aiChat) } + private let internalUserDecider: InternalUserDecider + private let userDefaults: UserDefaults - init(privacyConfigurationManager: PrivacyConfigurationManaging) { + init(privacyConfigurationManager: PrivacyConfigurationManaging, internalUserDecider: InternalUserDecider, userDefaults: UserDefaults = .standard) { + self.internalUserDecider = internalUserDecider self.privacyConfigurationManager = privacyConfigurationManager + self.userDefaults = userDefaults } // MARK: - Public @@ -53,10 +57,29 @@ struct AIChatRemoteSettings: AIChatRemoteSettingsProvider { return url } + var isAIChatBrowsingMenuUserSettingsEnabled: Bool { + userDefaults.showAIChatBrowsingMenu + } + + var isAIChatFeatureEnabled: Bool { + privacyConfigurationManager.privacyConfig.isEnabled(featureKey: .aiChat) || internalUserDecider.isInternalUser + } + + var isAIChatBrowsingToolbarShortcutFeatureEnabled: Bool { + let isBrowsingToolbarShortcutFeatureFlagEnabled = privacyConfigurationManager.privacyConfig.isSubfeatureEnabled(AIChatSubfeature.browsingToolbarShortcut) + let isInternalUser = internalUserDecider.isInternalUser + let isFeatureEnabled = isBrowsingToolbarShortcutFeatureFlagEnabled || isInternalUser + return isFeatureEnabled && isAIChatBrowsingMenuUserSettingsEnabled + } + + func enableAIChatBrowsingMenuUserSettings(enable: Bool) { + userDefaults.showAIChatBrowsingMenu = enable + } + // MARK: - Private private func getSettingsData(_ value: SettingsValue) -> String { - if let value = settings[value.rawValue] as? String { + if let value = remoteSettings[value.rawValue] as? String { return value } else { Pixel.fire(pixel: .aiChatNoRemoteSettingsFound(settings: value.rawValue)) @@ -64,3 +87,22 @@ struct AIChatRemoteSettings: AIChatRemoteSettingsProvider { } } } + +private extension UserDefaults { + enum Keys { + static let showAIChatBrowsingMenu = "aichat.settings.showAIChatBrowsingMenu" + } + + static let showAIChatBrowsingMenuDefaultValue = true + + @objc dynamic var showAIChatBrowsingMenu: Bool { + get { + value(forKey: Keys.showAIChatBrowsingMenu) as? Bool ?? Self.showAIChatBrowsingMenuDefaultValue + } + + set { + guard newValue != showAIChatBrowsingMenu else { return } + set(newValue, forKey: Keys.showAIChatBrowsingMenu) + } + } +} diff --git a/DuckDuckGo/MainViewController+Segues.swift b/DuckDuckGo/MainViewController+Segues.swift index 5eafe2cdea..beee686007 100644 --- a/DuckDuckGo/MainViewController+Segues.swift +++ b/DuckDuckGo/MainViewController+Segues.swift @@ -296,6 +296,9 @@ extension MainViewController { fireproofing: fireproofing, websiteDataManager: websiteDataManager) + let aiChatSettings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider) + let settingsViewModel = SettingsViewModel(legacyViewProvider: legacyViewProvider, subscriptionManager: AppDependencyProvider.shared.subscriptionManager, subscriptionFeatureAvailability: subscriptionFeatureAvailability, @@ -304,7 +307,8 @@ extension MainViewController { historyManager: historyManager, syncPausedStateManager: syncPausedStateManager, privacyProDataReporter: privacyProDataReporter, - textZoomCoordinator: textZoomCoordinator) + textZoomCoordinator: textZoomCoordinator, + aiChatSettings: aiChatSettings) Pixel.fire(pixel: .settingsPresented) if let navigationController = self.presentedViewController as? UINavigationController, diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 61f609df20..f4c0439fb2 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -188,8 +188,9 @@ class MainViewController: UIViewController { var appDidFinishLaunchingStartTime: CFAbsoluteTime? private lazy var aiChatNavigationController: UINavigationController = { - let remoteSettings = AIChatRemoteSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager) - let aiChatViewController = AIChatViewController(remoteSettings: remoteSettings, + let settings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider) + let aiChatViewController = AIChatViewController(settings: settings, webViewConfiguration: WKWebViewConfiguration.persistent(), pixelHandler: AIChatPixelHandler()) aiChatViewController.delegate = self diff --git a/DuckDuckGo/Settings.xcassets/Images/SettingsAIChat.imageset/Contents.json b/DuckDuckGo/Settings.xcassets/Images/SettingsAIChat.imageset/Contents.json new file mode 100644 index 0000000000..356f0fd6ea --- /dev/null +++ b/DuckDuckGo/Settings.xcassets/Images/SettingsAIChat.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SettingsAIChat.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Settings.xcassets/Images/SettingsAIChat.imageset/SettingsAIChat.pdf b/DuckDuckGo/Settings.xcassets/Images/SettingsAIChat.imageset/SettingsAIChat.pdf new file mode 100644 index 0000000000..adad6866a4 Binary files /dev/null and b/DuckDuckGo/Settings.xcassets/Images/SettingsAIChat.imageset/SettingsAIChat.pdf differ diff --git a/DuckDuckGo/SettingsAIChatView.swift b/DuckDuckGo/SettingsAIChatView.swift new file mode 100644 index 0000000000..2370bd4b63 --- /dev/null +++ b/DuckDuckGo/SettingsAIChatView.swift @@ -0,0 +1,54 @@ +// +// SettingsAIChatView.swift +// DuckDuckGo +// +// 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 SwiftUI +import DesignResourcesKit + +struct SettingsAIChatView: View { + @EnvironmentObject var viewModel: SettingsViewModel + + var body: some View { + List { + + VStack(alignment: .center) { + Image("SettingsDuckPlayerHero") + .padding(.top, -20) // Change this to AI Chat image + + Text(UserText.aiChatFeatureName) + .daxTitle3() + + Text(.init(UserText.aiChatPreferencesCaptionWithLinkMarkdown)) + .tint(Color.init(designSystemColor: .accent)) + .daxBodyRegular() + .multilineTextAlignment(.center) + .foregroundColor(Color(designSystemColor: .textSecondary)) + .padding(.top, 12) + } + .frame(maxWidth: .infinity) + .listRowBackground(Color.clear) + + Section { + SettingsCellView(label: UserText.aiChatSettingsEnableBrowsingMenuToggle, + accessory: .toggle(isOn: viewModel.aiChatEnabledBinding)) + } + }.applySettingsListModifiers(title: UserText.aiChatFeatureName, + displayMode: .inline, + viewModel: viewModel) + } +} diff --git a/DuckDuckGo/SettingsMainSettingsView.swift b/DuckDuckGo/SettingsMainSettingsView.swift index 02b487031b..e1e598356f 100644 --- a/DuckDuckGo/SettingsMainSettingsView.swift +++ b/DuckDuckGo/SettingsMainSettingsView.swift @@ -67,7 +67,15 @@ struct SettingsMainSettingsView: View { SettingsCellView(label: UserText.dataClearing, image: Image("SettingsDataClearing")) } - + + // AI Chat + if viewModel.state.aiChatEnabled { + NavigationLink(destination: SettingsAIChatView().environmentObject(viewModel)) { + SettingsCellView(label: UserText.aiChatFeatureName, + image: Image("SettingsAIChat")) + } + } + // Duck Player // We need to hide the settings until the user is enrolled in the experiment if viewModel.state.duckPlayerEnabled { diff --git a/DuckDuckGo/SettingsRootView.swift b/DuckDuckGo/SettingsRootView.swift index 0378363566..01ad7b6a60 100644 --- a/DuckDuckGo/SettingsRootView.swift +++ b/DuckDuckGo/SettingsRootView.swift @@ -129,6 +129,8 @@ struct SettingsRootView: View { SettingsDuckPlayerView().environmentObject(viewModel) case .netP: NetworkProtectionRootView() + case .aiChat: + SettingsAIChatView().environmentObject(viewModel) } } diff --git a/DuckDuckGo/SettingsState.swift b/DuckDuckGo/SettingsState.swift index 6d1fb7c43d..af6d1a27bb 100644 --- a/DuckDuckGo/SettingsState.swift +++ b/DuckDuckGo/SettingsState.swift @@ -102,7 +102,10 @@ struct SettingsState { var duckPlayerMode: DuckPlayerMode? var duckPlayerOpenInNewTab: Bool var duckPlayerOpenInNewTabEnabled: Bool - + + // AI Chat + var aiChatEnabled: Bool + static var defaults: SettingsState { return SettingsState( appTheme: .systemDefault, @@ -142,7 +145,8 @@ struct SettingsState { duckPlayerEnabled: false, duckPlayerMode: .alwaysAsk, duckPlayerOpenInNewTab: true, - duckPlayerOpenInNewTabEnabled: false + duckPlayerOpenInNewTabEnabled: false, + aiChatEnabled: false ) } } diff --git a/DuckDuckGo/SettingsViewModel.swift b/DuckDuckGo/SettingsViewModel.swift index a97700ad72..935eb18ca5 100644 --- a/DuckDuckGo/SettingsViewModel.swift +++ b/DuckDuckGo/SettingsViewModel.swift @@ -28,6 +28,7 @@ import DuckPlayer import Subscription import NetworkProtection +import AIChat final class SettingsViewModel: ObservableObject { @@ -44,6 +45,7 @@ final class SettingsViewModel: ObservableObject { private let historyManager: HistoryManaging let privacyProDataReporter: PrivacyProDataReporting? let textZoomCoordinator: TextZoomCoordinating + let aiChatSettings: AIChatSettingsProvider // Subscription Dependencies let subscriptionManager: SubscriptionManager @@ -259,6 +261,15 @@ final class SettingsViewModel: ObservableObject { ) } + var aiChatEnabledBinding: Binding { + Binding( + get: { self.aiChatSettings.isAIChatBrowsingMenuUserSettingsEnabled }, + set: { newValue in + self.aiChatSettings.enableAIChatBrowsingMenuUserSettings(enable: newValue) + } + ) + } + var textZoomLevelBinding: Binding { Binding( get: { self.state.textZoom.level }, @@ -386,7 +397,8 @@ final class SettingsViewModel: ObservableObject { historyManager: HistoryManaging, syncPausedStateManager: any SyncPausedStateManaging, privacyProDataReporter: PrivacyProDataReporting, - textZoomCoordinator: TextZoomCoordinating) { + textZoomCoordinator: TextZoomCoordinating, + aiChatSettings: AIChatSettingsProvider) { self.state = SettingsState.defaults self.legacyViewProvider = legacyViewProvider @@ -398,6 +410,7 @@ final class SettingsViewModel: ObservableObject { self.syncPausedStateManager = syncPausedStateManager self.privacyProDataReporter = privacyProDataReporter self.textZoomCoordinator = textZoomCoordinator + self.aiChatSettings = aiChatSettings setupNotificationObservers() updateRecentlyVisitedSitesVisibility() @@ -447,8 +460,9 @@ extension SettingsViewModel { duckPlayerEnabled: featureFlagger.isFeatureOn(.duckPlayer) || shouldDisplayDuckPlayerContingencyMessage, duckPlayerMode: appSettings.duckPlayerMode, duckPlayerOpenInNewTab: appSettings.duckPlayerOpenInNewTab, - duckPlayerOpenInNewTabEnabled: featureFlagger.isFeatureOn(.duckPlayerOpenInNewTab) - + duckPlayerOpenInNewTabEnabled: featureFlagger.isFeatureOn(.duckPlayerOpenInNewTab), + aiChatEnabled: aiChatSettings.isAIChatFeatureEnabled + ) updateRecentlyVisitedSitesVisibility() @@ -665,6 +679,7 @@ extension SettingsViewModel { case subscriptionFlow(origin: String? = nil) case restoreFlow case duckPlayer + case aiChat // Add other cases as needed var id: String { @@ -675,6 +690,7 @@ extension SettingsViewModel { case .subscriptionFlow: return "subscriptionFlow" case .restoreFlow: return "restoreFlow" case .duckPlayer: return "duckPlayer" + case .aiChat: return "aiChat" // Ensure all cases are covered } } @@ -683,7 +699,7 @@ extension SettingsViewModel { // Default to .sheet, specify .push where needed var type: DeepLinkType { switch self { - case .netP, .dbp, .itr, .subscriptionFlow, .restoreFlow, .duckPlayer: + case .netP, .dbp, .itr, .subscriptionFlow, .restoreFlow, .duckPlayer, .aiChat: return .navigationLink } } diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 3981ad71d3..7bb7182e8d 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -29,8 +29,11 @@ import PrivacyDashboard extension TabViewController { private var shouldShowAIChatInMenuHeader: Bool { - featureFlagger.isFeatureOn(.aiChatBrowsingToolbarShortcut) || AppDependencyProvider.shared.internalUserDecider.isInternalUser + let settings = AIChatSettings(privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager, + internalUserDecider: AppDependencyProvider.shared.internalUserDecider) + return settings.isAIChatBrowsingToolbarShortcutFeatureEnabled } + private var shouldShowPrintButtonInBrowsingMenu: Bool { shouldShowAIChatInMenuHeader } func buildBrowsingMenuHeaderContent() -> [BrowsingMenuEntry] { @@ -102,10 +105,6 @@ extension TabViewController { let linkEntries = buildLinkEntries(with: bookmarksInterface) entries.append(contentsOf: linkEntries) - if let domain = self.privacyInfo?.domain { - entries.append(self.buildToggleProtectionEntry(forDomain: domain)) - } - if shouldShowPrintButtonInBrowsingMenu { entries.append(.regular(name: UserText.actionPrintSite, accessibilityLabel: UserText.actionPrintSite, @@ -116,6 +115,10 @@ extension TabViewController { })) } + if let domain = self.privacyInfo?.domain { + entries.append(self.buildToggleProtectionEntry(forDomain: domain)) + } + if link != nil { let name = UserText.actionReportBrokenSite entries.append(BrowsingMenuEntry.regular(name: name, diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index 0445113d7c..6396d579da 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1315,6 +1315,13 @@ But if you *do* want a peek under the hood, you can find more information about // MARK: - AI Chat public static let aiChatTitle = NSLocalizedString("aichat.title", value: "DuckDuckGo AI Chat", comment: "Title for DuckDuckGo AI Chat. Should not be translated") + public static let aiChatFeatureName = NSLocalizedString("aichat.settings.title", value: "AI Chat", comment: "Settings screen cell text for AI Chat settings") + + public static let aiChatSettingsEnableFooter = NSLocalizedString("aichat.settings.enable.footer", value: "Turning this off will hide the AI Chat feature in the DuckDuckGo app.", comment: "Footer text for AI Chat settings") + public static let aiChatSettingsEnableBrowsingMenuToggle = NSLocalizedString("aichat.settings.enable.browsing-menu-toggle", value: "Show AI Chat in Browsing Menu", comment: "Toggle text to enable/disable AI Chat in the browsing menu") + + static let aiChatPreferencesCaptionWithLinkMarkdown = NSLocalizedString("ai-chat.preferences.caption.link.markdown", value: "AI Chat is an optional feature available at [duck.ai](https://duck.ai) that lets you have private conversations with popular 3rd-party AI chat models. Your chats are not used to train chat models.", comment: "Ai Chat preferences explanation with a markdown link. Do not translate what's inside [] and ()") + // MARK: - New Tab Page diff --git a/LocalPackages/AIChat/Sources/AIChat/AIChatViewModel.swift b/LocalPackages/AIChat/Sources/AIChat/AIChatViewModel.swift index 4d003227f2..36c326dd8a 100644 --- a/LocalPackages/AIChat/Sources/AIChat/AIChatViewModel.swift +++ b/LocalPackages/AIChat/Sources/AIChat/AIChatViewModel.swift @@ -43,7 +43,7 @@ protocol AIChatViewModeling { final class AIChatViewModel: AIChatViewModeling { - private let remoteSettings: AIChatRemoteSettingsProvider + private let settings: AIChatSettingsProvider private var cleanupTimerCancellable: AnyCancellable? let webViewConfiguration: WKWebViewConfiguration @@ -51,10 +51,10 @@ final class AIChatViewModel: AIChatViewModeling { let cleanupTime: TimeInterval - init(webViewConfiguration: WKWebViewConfiguration, remoteSettings: AIChatRemoteSettingsProvider, cleanupTime: TimeInterval = 600) { + init(webViewConfiguration: WKWebViewConfiguration, settings: AIChatSettingsProvider, cleanupTime: TimeInterval = 600) { self.cleanupTime = cleanupTime self.webViewConfiguration = webViewConfiguration - self.remoteSettings = remoteSettings + self.settings = settings } func cancelTimer() { @@ -76,6 +76,6 @@ final class AIChatViewModel: AIChatViewModeling { } var aiChatURL: URL { - remoteSettings.aiChatURL + settings.aiChatURL } } diff --git a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatRemoteSettingsProvider.swift b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatSettingsProvider.swift similarity index 58% rename from LocalPackages/AIChat/Sources/AIChat/Public API/AIChatRemoteSettingsProvider.swift rename to LocalPackages/AIChat/Sources/AIChat/Public API/AIChatSettingsProvider.swift index 7182584e28..afd76aaf9b 100644 --- a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatRemoteSettingsProvider.swift +++ b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatSettingsProvider.swift @@ -1,5 +1,5 @@ // -// AIChatRemoteSettingsProvider.swift +// AIChatSettingsProvider.swift // DuckDuckGo // // Copyright © 2024 DuckDuckGo. All rights reserved. @@ -19,7 +19,19 @@ import Foundation -public protocol AIChatRemoteSettingsProvider { +public protocol AIChatSettingsProvider { /// The URL used to open AI Chat in `AIChatViewController`. var aiChatURL: URL { get } + + /// User settings state for AI Chat browsing menu icon + var isAIChatBrowsingMenuUserSettingsEnabled: Bool { get } + + /// Remote feature flag state for AI Chat + var isAIChatFeatureEnabled: Bool { get } + + /// Remote feature flag for AI Chat shortcut in browsing menu + var isAIChatBrowsingToolbarShortcutFeatureEnabled: Bool { get } + + /// Update user settings state for AI Chat browsing menu + func enableAIChatBrowsingMenuUserSettings(enable: Bool) } diff --git a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift index 6210744202..4ec1ddba54 100644 --- a/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift +++ b/LocalPackages/AIChat/Sources/AIChat/Public API/AIChatViewController.swift @@ -41,11 +41,11 @@ public final class AIChatViewController: UIViewController { /// Initializes a new instance of `AIChatViewController` with the specified remote settings and web view configuration. /// /// - Parameters: - /// - remoteSettings: An object conforming to `AIChatRemoteSettingsProvider` that provides remote settings. + /// - remoteSettings: An object conforming to `AIChatSettingsProvider` that provides remote settings. /// - webViewConfiguration: A `WKWebViewConfiguration` object used to configure the web view. /// - pixelHandler: A `AIChatPixelHandling` object used to send pixel events. - public convenience init(remoteSettings: AIChatRemoteSettingsProvider, webViewConfiguration: WKWebViewConfiguration, pixelHandler: AIChatPixelHandling) { - let chatModel = AIChatViewModel(webViewConfiguration: webViewConfiguration, remoteSettings: remoteSettings) + public convenience init(settings: AIChatSettingsProvider, webViewConfiguration: WKWebViewConfiguration, pixelHandler: AIChatPixelHandling) { + let chatModel = AIChatViewModel(webViewConfiguration: webViewConfiguration, settings: settings) self.init(chatModel: chatModel, pixelHandler: pixelHandler) }