From 6adffd3391907bc960294196fa135a1cc61dce34 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 31 Oct 2024 11:25:55 +0100 Subject: [PATCH 01/42] Define experimental feature and enable switching between HTML and native NTP --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++ DuckDuckGo/Application/AppDelegate.swift | 5 ++ .../Application/ExperimentalFeatures.swift | 70 +++++++++++++++++++ .../Utilities/UserDefaultsWrapper.swift | 4 ++ DuckDuckGo/Menus/MainMenu.swift | 13 ++++ .../Tab/View/BrowserTabViewController.swift | 17 ++++- .../Updates/ReleaseNotesUserScript.swift | 2 +- .../YoutubePlayer/DuckURLSchemeHandler.swift | 11 ++- 8 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 DuckDuckGo/Application/ExperimentalFeatures.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ed21f5ef87..0e48e66331 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1194,6 +1194,8 @@ 37A6A8F22AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */; }; 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */; }; 37A6A8F72AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */; }; + 37A746A62CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */; }; + 37A746A72CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */; }; 37A803DB27FD69D300052F4C /* DataImportResources in Resources */ = {isa = PBXBuildFile; fileRef = 37A803DA27FD69D300052F4C /* DataImportResources */; }; 37AAA41C2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */; }; 37AAA41D2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */; }; @@ -3586,6 +3588,7 @@ 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartupPreferences.swift; sourceTree = ""; }; 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboarding.swift; sourceTree = ""; }; 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboardingViewController.swift; sourceTree = ""; }; + 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeatures.swift; sourceTree = ""; }; 37A803DA27FD69D300052F4C /* DataImportResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DataImportResources; sourceTree = ""; }; 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarTextFieldView.swift; sourceTree = ""; }; 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; @@ -7566,6 +7569,7 @@ AA4D700525545EDE00C3411E /* Application */ = { isa = PBXGroup; children = ( + 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */, 4B4D60E12A0C883A00BCD287 /* AppMain.swift */, B62B48382ADE46FC000DECE5 /* Application.swift */, AA585D81248FD31100E9A3E2 /* AppDelegate.swift */, @@ -10927,6 +10931,7 @@ 3706FA9F293F65D500E42796 /* FeedbackPresenter.swift in Sources */, CD2AB5C42C8222F70019EB49 /* PhishingDetectionStateManager.swift in Sources */, 37A6A8F22AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */, + 37A746A72CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */, 859F30652A72A9FA00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 37197EA22942441900394917 /* Tab+Dialogs.swift in Sources */, 3706FAA0293F65D500E42796 /* UserAgent.swift in Sources */, @@ -13069,6 +13074,7 @@ B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */, B68458C525C7EA0C00DC17B6 /* TabCollection+NSSecureCoding.swift in Sources */, + 37A746A62CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */, C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 370C23072C76A36500A80A3E /* BackgroundPickerView.swift in Sources */, 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 14ae61f423..093cb67bcb 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -44,6 +44,11 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { + let experimentalFeatures = ExperimentalFeatures() + @objc func toggleHTMLNTP(_ sender: NSMenuItem) { + experimentalFeatures.isHTMLNewTabPageEnabled.toggle() + } + #if DEBUG let disableCVDisplayLinkLogs: Void = { // Disable CVDisplayLink logs diff --git a/DuckDuckGo/Application/ExperimentalFeatures.swift b/DuckDuckGo/Application/ExperimentalFeatures.swift new file mode 100644 index 0000000000..29d95b145c --- /dev/null +++ b/DuckDuckGo/Application/ExperimentalFeatures.swift @@ -0,0 +1,70 @@ +// +// ExperimentalFeatures.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 Foundation + +protocol ExperimentalFeaturesPersistor { + var isHTMLNewTabPageEnabled: Bool { get set } +} + +struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor { + @UserDefaultsWrapper(key: .htmlNewTabPage, defaultValue: false) + var isHTMLNewTabPageEnabled: Bool +} + +protocol ExperimentalFeaturesHandler { + func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) +} + +struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { + func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) { + Task { @MainActor in + WindowControllersManager.shared.mainWindowControllers.forEach { mainWindowController in + if mainWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab { + mainWindowController.mainViewController.browserTabViewController.refreshTab() + } + } + } + } +} + +final class ExperimentalFeatures { + + private var persistor: ExperimentalFeaturesPersistor + private var actionHandler: ExperimentalFeaturesHandler + + init( + persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(), + actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() + ) { + self.persistor = persistor + self.actionHandler = actionHandler + } + + var isHTMLNewTabPageEnabled: Bool { + get { + persistor.isHTMLNewTabPageEnabled + } + set { + if newValue != persistor.isHTMLNewTabPageEnabled { + persistor.isHTMLNewTabPageEnabled = newValue + actionHandler.isHTMLNewTabPageEnabledDidChange(newValue) + } + } + } +} diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index a18850b028..56c3adc634 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -224,6 +224,10 @@ public struct UserDefaultsWrapper { // Subscription case subscriptionEnvironment = "subscription.environment" + + // Experimental Feature Flags + + case htmlNewTabPage = "experimental.html-new-tab-page" } enum RemovedKeys: String, CaseIterable { diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index ce940baf61..f97c1a3a20 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -91,6 +91,7 @@ final class MainMenu: NSMenu { let windowsMenu = NSMenu(title: UserText.mainMenuWindow) // MARK: Debug + private var experimentalFeaturesMenu: NSMenu? private var loggingMenu: NSMenu? let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil) @@ -446,6 +447,11 @@ final class MainMenu: NSMenu { updateInternalUserItem() updateRemoteConfigurationInfo() updateAutofillDebugScriptMenuItem() + updateExperimentalFeatures() + } + + private func updateExperimentalFeatures() { + experimentalFeaturesMenu?.items[0].state = NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled ? .on : .off } // MARK: - Bookmarks @@ -617,7 +623,14 @@ final class MainMenu: NSMenu { @MainActor private func setupDebugMenu() -> NSMenu { + let experimentalFeaturesMenu = NSMenu() { + NSMenuItem(title: "HTML New Tab Page", action: #selector(AppDelegate.toggleHTMLNTP(_:)), representedObject: 10) + } + self.experimentalFeaturesMenu = experimentalFeaturesMenu let debugMenu = NSMenu(title: "Debug") { + NSMenuItem(title: "Experimental features") + .submenu(experimentalFeaturesMenu) + NSMenuItem.separator() NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() NSMenuItem(title: "Tab") { diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 46667bc167..2d8feb22dc 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -777,8 +777,12 @@ final class BrowserTabViewController: NSViewController { updateTabIfNeeded(tabViewModel: tabViewModel) case .newtab: - removeAllTabContent() - addAndLayoutChild(homePageViewControllerCreatingIfNeeded()) + if NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled { + updateTabIfNeeded(tabViewModel: tabViewModel) + } else { + removeAllTabContent() + addAndLayoutChild(homePageViewControllerCreatingIfNeeded()) + } case .dataBrokerProtection: removeAllTabContent() @@ -790,6 +794,11 @@ final class BrowserTabViewController: NSViewController { } } + func refreshTab() { + guard let tabViewModel else { return } + showTabContent(of: tabViewModel) + } + func updateTabIfNeeded(tabViewModel: TabViewModel?) { if shouldReplaceWebView(for: tabViewModel) { removeAllTabContent(includingWebView: true) @@ -835,7 +844,9 @@ final class BrowserTabViewController: NSViewController { switch tabViewModel.tab.content { case .onboarding: return - case .newtab, .settings: + case .newtab: + containsHostingView = !NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled + case .settings: containsHostingView = true default: containsHostingView = false diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 620c2ff3b9..16e7dc7e02 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -26,7 +26,7 @@ import Combine final class ReleaseNotesUserScript: NSObject, Subfeature { lazy var updateController: UpdateControllerProtocol = Application.appDelegate.updateController - var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes")]) + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes"), .exact(hostname: "newtab")]) let featureName: String = "release-notes" weak var broker: UserScriptMessageBroker? weak var webView: WKWebView? { diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index e169c255e5..aac4abfb3f 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -30,7 +30,7 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { } switch requestURL.type { - case .onboarding, .releaseNotes: + case .onboarding, .releaseNotes, .newTab: handleSpecialPages(urlSchemeTask: urlSchemeTask) case .duckPlayer: handleDuckPlayer(requestURL: requestURL, urlSchemeTask: urlSchemeTask, webView: webView) @@ -127,6 +127,8 @@ private extension DuckURLSchemeHandler { directoryURL = URL(fileURLWithPath: "/pages/onboarding") } else if url.isReleaseNotesScheme { directoryURL = URL(fileURLWithPath: "/pages/release-notes") + } else if url.isNewTab { + directoryURL = URL(fileURLWithPath: "/pages/new-tab") } else { assertionFailure("Unknown scheme") return nil @@ -205,6 +207,7 @@ private extension DuckURLSchemeHandler { extension URL { enum URLType { + case newTab case onboarding case duckPlayer case releaseNotes @@ -220,6 +223,8 @@ extension URL { return .phishingErrorPage } else if self.isReleaseNotesScheme { return .releaseNotes + } else if self.isNewTab { + return .newTab } else { return nil } @@ -229,6 +234,10 @@ extension URL { return isDuckURLScheme && host == "onboarding" } + var isNewTab: Bool { + return isDuckURLScheme && host == "newtab" + } + var isDuckURLScheme: Bool { navigationalScheme == .duck } From 53afdcebd4a9c3c4b5d943f554985fec6c933011 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 6 Nov 2024 13:33:24 +0100 Subject: [PATCH 02/42] Add NewTabPageUserScript with init action --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++ .../ScriptSourceProviding.swift | 8 ++ .../HomePage/NewTabPageActionsManager.swift | 99 +++++++++++++ .../HomePage/NewTabPageUserScript.swift | 131 ++++++++++++++++++ .../SpecialPagesUserScriptExtension.swift | 13 ++ DuckDuckGo/Tab/UserScripts/UserScripts.swift | 6 + 6 files changed, 269 insertions(+) create mode 100644 DuckDuckGo/HomePage/NewTabPageActionsManager.swift create mode 100644 DuckDuckGo/HomePage/NewTabPageUserScript.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 0e48e66331..51d8d0868d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1263,6 +1263,10 @@ 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */; }; 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */; }; 37F44A5F298C17830025E7FE /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 37F44A5E298C17830025E7FE /* Navigation */; }; + 37FB430E2CDB84A500479A1E /* NewTabPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */; }; + 37FB430F2CDB84A500479A1E /* NewTabPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */; }; + 37FB43112CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */; }; + 37FB43122CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */; }; 37FD78112A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; @@ -3643,6 +3647,8 @@ 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDuckPlayerView.swift; sourceTree = ""; }; 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerPreferences.swift; sourceTree = ""; }; 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayer.swift; sourceTree = ""; }; + 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageUserScript.swift; sourceTree = ""; }; + 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageActionsManager.swift; sourceTree = ""; }; 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; 4B02197F25E05FAC00ED7DEA /* FireproofingURLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofingURLExtensions.swift; sourceTree = ""; }; @@ -8617,6 +8623,8 @@ AAE71DB225F66A0900D74437 /* HomePage */ = { isa = PBXGroup; children = ( + 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */, + 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */, 85589E8527BBB8DD0038AD11 /* Model */, AAE71DB325F66A3F00D74437 /* View */, 85AC7ADA27BD628400FFB69B /* HomePage.swift */, @@ -11019,6 +11027,7 @@ EED4D3D92C874AE200C79EEA /* PopoverInfoViewController.swift in Sources */, 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */, 3706FAD9293F65D500E42796 /* FirefoxFaviconsReader.swift in Sources */, + 37FB430E2CDB84A500479A1E /* NewTabPageUserScript.swift in Sources */, 3706FADB293F65D500E42796 /* ContentBlockingRulesUpdateObserver.swift in Sources */, 3706FADC293F65D500E42796 /* FirefoxLoginReader.swift in Sources */, 3706FADD293F65D500E42796 /* AtbParser.swift in Sources */, @@ -11575,6 +11584,7 @@ 3706FC16293F65D500E42796 /* PasswordManagementLoginModel.swift in Sources */, 3706FC17293F65D500E42796 /* TabViewModel.swift in Sources */, 3706FC18293F65D500E42796 /* TabDragAndDropManager.swift in Sources */, + 37FB43112CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */, 1DC669712B6CF0D700AA0645 /* TabSnapshotStore.swift in Sources */, 3706FC19293F65D500E42796 /* NSNotificationName+Favicons.swift in Sources */, 3706FC1A293F65D500E42796 /* PinningManager.swift in Sources */, @@ -12783,6 +12793,7 @@ 37054FCE2876472D00033B6F /* WebViewSnapshotView.swift in Sources */, 560EB9392C789A450080DBC8 /* OnboardingSuggestedSearchesProvider.swift in Sources */, 4BBC16A027C4859400E00A38 /* DeviceAuthenticationService.swift in Sources */, + 37FB43122CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */, C181945C2C7CDCC700381092 /* PromotionView.swift in Sources */, CB24F70C29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, 1DEF3BAD2BD145A9004A2FBA /* AutoClearHandler.swift in Sources */, @@ -13182,6 +13193,7 @@ B6ABD0CA2BC03F610000EB69 /* SecurityScopedFileURLController.swift in Sources */, B6040856274B830F00680351 /* DictionaryExtension.swift in Sources */, 3199AF772C80734A003AEBDC /* DuckPlayerOnboardingViewController.swift in Sources */, + 37FB430F2CDB84A500479A1E /* NewTabPageUserScript.swift in Sources */, B684592725C93C0500DC17B6 /* Publishers.NestedObjectChanges.swift in Sources */, B6DA06E62913F39400225DE2 /* MenuItemSelectors.swift in Sources */, 3712092C2C23383C003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index 4e3cad6696..b5156afa65 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -31,6 +31,7 @@ protocol ScriptSourceProviding { var autofillSourceProvider: AutofillUserScriptSourceProvider? { get } var sessionKey: String? { get } var onboardingActionsManager: OnboardingActionsManaging? { get } + var newTabPageActionsManager: NewTabPageActionsManaging? { get } func buildAutofillSource() -> AutofillUserScriptSourceProvider } @@ -45,6 +46,7 @@ struct ScriptSourceProvider: ScriptSourceProviding { private(set) var contentBlockerRulesConfig: ContentBlockerUserScriptConfig? private(set) var surrogatesConfig: SurrogatesUserScriptConfig? private(set) var onboardingActionsManager: OnboardingActionsManaging? + private(set) var newTabPageActionsManager: NewTabPageActionsManaging? private(set) var autofillSourceProvider: AutofillUserScriptSourceProvider? private(set) var sessionKey: String? @@ -75,6 +77,7 @@ struct ScriptSourceProvider: ScriptSourceProviding { self.sessionKey = generateSessionKey() self.autofillSourceProvider = buildAutofillSource() self.onboardingActionsManager = buildOnboardingActionsManager() + self.newTabPageActionsManager = buildNewTabPageActionsManager() } private func generateSessionKey() -> String { @@ -138,6 +141,11 @@ struct ScriptSourceProvider: ScriptSourceProviding { startupPreferences: StartupPreferences.shared) } + @MainActor + private func buildNewTabPageActionsManager() -> NewTabPageActionsManaging { + NewTabPageActionsManager(appearancePreferences: .shared) + } + private func loadTextFile(_ fileName: String, _ fileExt: String) -> String? { let url = Bundle.main.url( forResource: fileName, diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift new file mode 100644 index 0000000000..8bee38af86 --- /dev/null +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -0,0 +1,99 @@ +// +// NewTabPageActionsManager.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 Foundation +import Combine +import PixelKit +import Common +import os.log + +protocol NewTabPageActionsManaging { + var configuration: NewTabPageConfiguration { get } + + /// It is called in case of error loading the pages + func reportException(with param: [String: String]) +} + +struct NewTabPageConfiguration: Encodable { + var widgets: [Widget] + var widgetConfigs: [WidgetConfig] + var env: String + var locale: String + var platform: Platform + + struct Widget: Encodable { + var id: String + } + + struct WidgetConfig: Encodable { + + enum WidgetVisibility: String, Encodable { + case visible, hidden + } + + init(id: String, isVisible: Bool) { + self.id = id + self.visibility = isVisible ? .visible : .hidden + } + + var id: String + var visibility: WidgetVisibility + } + + struct Platform: Encodable { + var name: String + } +} + +final class NewTabPageActionsManager: NewTabPageActionsManaging { + + private let appearancePreferences: AppearancePreferences + private var cancellables = Set() + + init(appearancePreferences: AppearancePreferences) { + self.appearancePreferences = appearancePreferences + } + + var configuration: NewTabPageConfiguration { +#if DEBUG || REVIEW + let env = "development" +#else + let env = "production" +#endif + return .init( + widgets: [ + .init(id: "rmf"), + .init(id: "favorites"), + .init(id: "privacyStats") + ], + widgetConfigs: [ + .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), + .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) + ], + env: env, + locale: Bundle.main.preferredLocalizations.first ?? "en", + platform: .init(name: "macos") + ) + } + + func reportException(with param: [String: String]) { + let message = param["message"] ?? "" + let id = param["id"] ?? "" + Logger.general.error("New Tab Page error: \("\(id): \(message)", privacy: .public)") + } +} diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift new file mode 100644 index 0000000000..feccc47bd0 --- /dev/null +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -0,0 +1,131 @@ +// +// NewTabPageUserScript.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 Foundation +import UserScript +import WebKit + +final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { + + let actionsManager: NewTabPageActionsManaging + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "newtab")]) + let featureName: String = "newTabPage" + weak var broker: UserScriptMessageBroker? + + // MARK: - MessageNames + enum MessageNames: String, CaseIterable { + case initialSetup + case reportInitException + } + + init(actionsManager: NewTabPageActionsManaging) { + self.actionsManager = actionsManager + } + + public func with(broker: UserScriptMessageBroker) { + self.broker = broker + } + + private lazy var methodHandlers: [MessageNames: Handler] = [ + .initialSetup: initialSetup, + .reportInitException: reportException + ] + + @MainActor + func handler(forMethodNamed methodName: String) -> Handler? { + guard let messageName = MessageNames(rawValue: methodName) else { return nil } + return methodHandlers[messageName] + } + + // MARK: - UserValuesNotification + + struct UserValuesNotification: Encodable { + let userValuesNotification: UserValues + } + +} + +extension NewTabPageUserScript { + @MainActor + private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { + return actionsManager.configuration + } +// +// @MainActor +// private func dismissToAddressBar(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// onboardingActionsManager.goToAddressBar() +// return nil +// } +// +// @MainActor +// private func dismissToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// onboardingActionsManager.goToSettings() +// return nil +// } +// +// private func requestDockOptIn(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// onboardingActionsManager.addToDock() +// return Result() +// } +// +// @MainActor +// private func requestImport(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// onboardingActionsManager.importData() +// return Result() +// } +// +// private func requestSetAsDefault(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// onboardingActionsManager.setAsDefault() +// return Result() +// } +// +// @MainActor +// private func setBookmarksBar(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// guard let params = params as? [String: Bool], let enabled = params["enabled"] else { return nil } +// onboardingActionsManager.setBookmarkBar(enabled: enabled) +// return nil +// } +// +// private func setSessionRestore(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// guard let params = params as? [String: Bool], let enabled = params["enabled"] else { return nil } +// onboardingActionsManager.setSessionRestore(enabled: enabled) +// return nil +// } +// +// private func setShowHome(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// guard let params = params as? [String: Bool], let enabled = params["enabled"] else { return nil } +// onboardingActionsManager.setHomeButtonPosition(enabled: enabled) +// return nil +// } +// +// private func stepCompleted(params: Any, original: WKScriptMessage) async throws -> Encodable? { +// if let params = params as? [String: String], let stepString = params["id"], let step = OnboardingSteps(rawValue: stepString) { +// onboardingActionsManager.stepCompleted(step: step) +// } +// return nil +// } +// + private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let params = params as? [String: String] else { return nil } + actionsManager.reportException(with: params) + return nil + } + + struct Result: Encodable {} + +} diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index f5cff41746..ba9b91dbf9 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -28,6 +28,13 @@ extension SpecialPagesUserScript { self.registerSubfeature(delegate: onboardingScript) } + @MainActor + func withNewTabPage() { + let actionsManager = buildNewTabPageActionsManager() + let userScript = NewTabPageUserScript(actionsManager: actionsManager) + self.registerSubfeature(delegate: userScript) + } + func withDuckPlayerIfAvailable() { var youtubePlayerUserScript: YoutubePlayerUserScript? if DuckPlayer.shared.isAvailable { @@ -48,6 +55,7 @@ extension SpecialPagesUserScript { @MainActor func withAllSubfeatures() { withOnboarding() + withNewTabPage() withErrorPages() withDuckPlayerIfAvailable() } @@ -61,4 +69,9 @@ extension SpecialPagesUserScript { appearancePreferences: AppearancePreferences.shared, startupPreferences: StartupPreferences.shared) } + + @MainActor + private func buildNewTabPageActionsManager() -> NewTabPageActionsManaging { + NewTabPageActionsManager(appearancePreferences: .shared) + } } diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 970344e775..71f1885116 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -47,6 +47,7 @@ final class UserScripts: UserScriptsProvider { let youtubePlayerUserScript: YoutubePlayerUserScript? let specialErrorPageUserScript: SpecialErrorPageUserScript? let onboardingUserScript: OnboardingUserScript? + let newTabPageUserScript: NewTabPageUserScript? #if SPARKLE let releaseNotesUserScript: ReleaseNotesUserScript? #endif @@ -74,6 +75,7 @@ final class UserScripts: UserScriptsProvider { languageCode: lenguageCode) onboardingUserScript = OnboardingUserScript(onboardingActionsManager: sourceProvider.onboardingActionsManager!) + newTabPageUserScript = NewTabPageUserScript(actionsManager: sourceProvider.newTabPageActionsManager!) specialPages = SpecialPagesUserScript() @@ -112,6 +114,10 @@ final class UserScripts: UserScriptsProvider { if let onboardingUserScript { specialPages.registerSubfeature(delegate: onboardingUserScript) } + + if let newTabPageUserScript { + specialPages.registerSubfeature(delegate: newTabPageUserScript) + } userScripts.append(specialPages) } From e86c33879cf38c2a7b0abb07c1e54f9333b615f6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 6 Nov 2024 15:23:57 +0100 Subject: [PATCH 03/42] Use single instance of NTP user script and implement context menu --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++ DuckDuckGo/Application/AppDelegate.swift | 3 + .../ScriptSourceProviding.swift | 8 -- .../HomePage/NewTabPageActionsManager.swift | 98 ++++++++++++++++++- .../HomePage/NewTabPageTabExtension.swift | 59 +++++++++++ .../HomePage/NewTabPageUserScript.swift | 92 ++++++----------- .../SpecialPagesUserScriptExtension.swift | 4 +- .../Tab/TabExtensions/TabExtensions.swift | 4 + DuckDuckGo/Tab/UserScripts/UserScripts.swift | 2 +- .../YoutubePlayer/DuckURLSchemeHandler.swift | 6 +- 10 files changed, 200 insertions(+), 82 deletions(-) create mode 100644 DuckDuckGo/HomePage/NewTabPageTabExtension.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 51d8d0868d..e369144a6b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1267,6 +1267,8 @@ 37FB430F2CDB84A500479A1E /* NewTabPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */; }; 37FB43112CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */; }; 37FB43122CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */; }; + 37FB43142CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */; }; + 37FB43152CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */; }; 37FD78112A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; @@ -3649,6 +3651,7 @@ 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayer.swift; sourceTree = ""; }; 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageUserScript.swift; sourceTree = ""; }; 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageActionsManager.swift; sourceTree = ""; }; + 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageTabExtension.swift; sourceTree = ""; }; 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; 4B02197F25E05FAC00ED7DEA /* FireproofingURLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofingURLExtensions.swift; sourceTree = ""; }; @@ -8623,6 +8626,7 @@ AAE71DB225F66A0900D74437 /* HomePage */ = { isa = PBXGroup; children = ( + 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */, 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */, 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */, 85589E8527BBB8DD0038AD11 /* Model */, @@ -11810,6 +11814,7 @@ 3706FCA0293F65D500E42796 /* ContiguousBytesExtension.swift in Sources */, B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, + 37FB43142CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */, 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, @@ -13133,6 +13138,7 @@ 4BE65479271FCD41008D1D63 /* EditableTextView.swift in Sources */, AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */, 1DA84D322C119AE70011C80F /* UpdateMenuItemFactory.swift in Sources */, + 37FB43152CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */, B6B4D1CF2B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, B688B4DA273E6D3B0087BEAF /* MainView.swift in Sources */, B61E2CD5294346C000773D8A /* Tab+Navigation.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 093cb67bcb..2ea5b201a2 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -44,6 +44,9 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { + let newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared) + private(set) lazy var newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) + let experimentalFeatures = ExperimentalFeatures() @objc func toggleHTMLNTP(_ sender: NSMenuItem) { experimentalFeatures.isHTMLNewTabPageEnabled.toggle() diff --git a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift index b5156afa65..4e3cad6696 100644 --- a/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift +++ b/DuckDuckGo/ContentBlocker/ScriptSourceProviding.swift @@ -31,7 +31,6 @@ protocol ScriptSourceProviding { var autofillSourceProvider: AutofillUserScriptSourceProvider? { get } var sessionKey: String? { get } var onboardingActionsManager: OnboardingActionsManaging? { get } - var newTabPageActionsManager: NewTabPageActionsManaging? { get } func buildAutofillSource() -> AutofillUserScriptSourceProvider } @@ -46,7 +45,6 @@ struct ScriptSourceProvider: ScriptSourceProviding { private(set) var contentBlockerRulesConfig: ContentBlockerUserScriptConfig? private(set) var surrogatesConfig: SurrogatesUserScriptConfig? private(set) var onboardingActionsManager: OnboardingActionsManaging? - private(set) var newTabPageActionsManager: NewTabPageActionsManaging? private(set) var autofillSourceProvider: AutofillUserScriptSourceProvider? private(set) var sessionKey: String? @@ -77,7 +75,6 @@ struct ScriptSourceProvider: ScriptSourceProviding { self.sessionKey = generateSessionKey() self.autofillSourceProvider = buildAutofillSource() self.onboardingActionsManager = buildOnboardingActionsManager() - self.newTabPageActionsManager = buildNewTabPageActionsManager() } private func generateSessionKey() -> String { @@ -141,11 +138,6 @@ struct ScriptSourceProvider: ScriptSourceProviding { startupPreferences: StartupPreferences.shared) } - @MainActor - private func buildNewTabPageActionsManager() -> NewTabPageActionsManaging { - NewTabPageActionsManager(appearancePreferences: .shared) - } - private func loadTextFile(_ fileName: String, _ fileExt: String) -> String? { let url = Bundle.main.url( forResource: fileName, diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index 8bee38af86..0ac75964fd 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -22,11 +22,14 @@ import PixelKit import Common import os.log -protocol NewTabPageActionsManaging { +protocol NewTabPageActionsManaging: AnyObject { var configuration: NewTabPageConfiguration { get } + var userScript: NewTabPageUserScript? { get set } /// It is called in case of error loading the pages - func reportException(with param: [String: String]) + func reportException(with params: [String: String]) + func showContextMenu(with params: [String: Any]) + func updateWidgetConfigs(with params: [[String: String]]) } struct NewTabPageConfiguration: Encodable { @@ -44,6 +47,10 @@ struct NewTabPageConfiguration: Encodable { enum WidgetVisibility: String, Encodable { case visible, hidden + + var isVisible: Bool { + self == .visible + } } init(id: String, isVisible: Bool) { @@ -64,9 +71,31 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { private let appearancePreferences: AppearancePreferences private var cancellables = Set() + weak var userScript: NewTabPageUserScript? init(appearancePreferences: AppearancePreferences) { self.appearancePreferences = appearancePreferences + + appearancePreferences.$isFavoriteVisible.dropFirst().removeDuplicates().asVoid() + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.notifyWidgetConfigsDidChange() + } + .store(in: &cancellables) + + appearancePreferences.$isRecentActivityVisible.dropFirst().removeDuplicates().asVoid() + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.notifyWidgetConfigsDidChange() + } + .store(in: &cancellables) + } + + private func notifyWidgetConfigsDidChange() { + userScript?.widgetConfigsUpdated(widgetConfigs: [ + .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), + .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) + ]) } var configuration: NewTabPageConfiguration { @@ -91,9 +120,68 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { ) } - func reportException(with param: [String: String]) { - let message = param["message"] ?? "" - let id = param["id"] ?? "" + func showContextMenu(with params: [String: Any]) { + guard let menuItems = params["visibilityMenuItems"] as? [[String: String]] else { + return + } + let menu = NSMenu() + + for menuItem in menuItems { + guard let title = menuItem["title"], let id = menuItem["id"] else { + continue + } + switch id { + case "favorites": + let item = NSMenuItem(title: title, action: #selector(toggleVisibility(_:)), representedObject: id) + .targetting(self) + item.state = appearancePreferences.isFavoriteVisible ? .on : .off + menu.addItem(item) + case "privacyStats": + let item = NSMenuItem(title: title, action: #selector(toggleVisibility(_:)), representedObject: id) + .targetting(self) + item.state = appearancePreferences.isRecentActivityVisible ? .on : .off + menu.addItem(item) + default: + break + } + } + + if !menu.items.isEmpty { + menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil) + } + } + + @objc private func toggleVisibility(_ sender: NSMenuItem) { + switch sender.representedObject as? String { + case "favorites": + appearancePreferences.isFavoriteVisible.toggle() + case "privacyStats": + appearancePreferences.isRecentActivityVisible.toggle() + default: + break + } + } + + func updateWidgetConfigs(with params: [[String: String]]) { + for param in params { + guard let id = param["id"], let visibility = param["visibility"] else { + continue + } + let isVisible = NewTabPageConfiguration.WidgetConfig.WidgetVisibility(rawValue: visibility)?.isVisible == true + switch id { + case "favorites": + appearancePreferences.isFavoriteVisible = isVisible + case "privacyStats": + appearancePreferences.isRecentActivityVisible = isVisible + default: + break + } + } + } + + func reportException(with params: [String: String]) { + let message = params["message"] ?? "" + let id = params["id"] ?? "" Logger.general.error("New Tab Page error: \("\(id): \(message)", privacy: .public)") } } diff --git a/DuckDuckGo/HomePage/NewTabPageTabExtension.swift b/DuckDuckGo/HomePage/NewTabPageTabExtension.swift new file mode 100644 index 0000000000..be60b2a89c --- /dev/null +++ b/DuckDuckGo/HomePage/NewTabPageTabExtension.swift @@ -0,0 +1,59 @@ +// +// NewTabPageTabExtension.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 +import Foundation +import Navigation + +protocol NewTabPageUserScriptProvider { + var newTabPageUserScript: NewTabPageUserScript? { get } +} +extension UserScripts: NewTabPageUserScriptProvider {} + +final class NewTabPageTabExtension: NavigationResponder { + + private weak var webView: WKWebView? { + didSet { + newTabPageUserScript?.webViews.add(webView) + } + } + private weak var newTabPageUserScript: NewTabPageUserScript? + private var cancellables: Set = [] + + init(scriptsPublisher: some Publisher, webViewPublisher: some Publisher) { + webViewPublisher.sink { [weak self] webView in + self?.webView = webView + }.store(in: &cancellables) + + scriptsPublisher.sink { [weak self] scripts in + guard let self else { return } + newTabPageUserScript = scripts.newTabPageUserScript + newTabPageUserScript?.webViews.add(webView) + }.store(in: &cancellables) + } +} + +protocol NewTabPageTabExtensionProtocol: AnyObject, NavigationResponder {} + +extension NewTabPageTabExtension: NewTabPageTabExtensionProtocol, TabExtension { + func getPublicProtocol() -> NewTabPageTabExtensionProtocol { self } +} + +extension TabExtensions { + var newTabPage: NewTabPageTabExtensionProtocol? { resolve(NewTabPageTabExtension.self) } +} diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index feccc47bd0..a1429dc909 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -26,15 +26,21 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "newtab")]) let featureName: String = "newTabPage" weak var broker: UserScriptMessageBroker? + var webViews: NSHashTable = .weakObjects() // MARK: - MessageNames enum MessageNames: String, CaseIterable { + case contextMenu case initialSetup case reportInitException + case reportPageException + case widgetsSetConfig = "widgets_setConfig" } init(actionsManager: NewTabPageActionsManaging) { self.actionsManager = actionsManager + super.init() + actionsManager.userScript = self } public func with(broker: UserScriptMessageBroker) { @@ -42,8 +48,11 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { } private lazy var methodHandlers: [MessageNames: Handler] = [ - .initialSetup: initialSetup, - .reportInitException: reportException + .contextMenu: { [weak self] in try await self?.showContextMenu(params: $0, original: $1) }, + .initialSetup: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, + .reportInitException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, + .reportPageException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, + .widgetsSetConfig: { [weak self] in try await self?.widgetsSetConfig(params: $0, original: $1) } ] @MainActor @@ -52,12 +61,11 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { return methodHandlers[messageName] } - // MARK: - UserValuesNotification - - struct UserValuesNotification: Encodable { - let userValuesNotification: UserValues + func widgetConfigsUpdated(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { + for webView in webViews.allObjects { + broker?.push(method: "widgets_onConfigUpdated", params: widgetConfigs, for: self, into: webView) + } } - } extension NewTabPageUserScript { @@ -65,61 +73,21 @@ extension NewTabPageUserScript { private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { return actionsManager.configuration } -// -// @MainActor -// private func dismissToAddressBar(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// onboardingActionsManager.goToAddressBar() -// return nil -// } -// -// @MainActor -// private func dismissToSettings(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// onboardingActionsManager.goToSettings() -// return nil -// } -// -// private func requestDockOptIn(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// onboardingActionsManager.addToDock() -// return Result() -// } -// -// @MainActor -// private func requestImport(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// onboardingActionsManager.importData() -// return Result() -// } -// -// private func requestSetAsDefault(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// onboardingActionsManager.setAsDefault() -// return Result() -// } -// -// @MainActor -// private func setBookmarksBar(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// guard let params = params as? [String: Bool], let enabled = params["enabled"] else { return nil } -// onboardingActionsManager.setBookmarkBar(enabled: enabled) -// return nil -// } -// -// private func setSessionRestore(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// guard let params = params as? [String: Bool], let enabled = params["enabled"] else { return nil } -// onboardingActionsManager.setSessionRestore(enabled: enabled) -// return nil -// } -// -// private func setShowHome(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// guard let params = params as? [String: Bool], let enabled = params["enabled"] else { return nil } -// onboardingActionsManager.setHomeButtonPosition(enabled: enabled) -// return nil -// } -// -// private func stepCompleted(params: Any, original: WKScriptMessage) async throws -> Encodable? { -// if let params = params as? [String: String], let stepString = params["id"], let step = OnboardingSteps(rawValue: stepString) { -// onboardingActionsManager.stepCompleted(step: step) -// } -// return nil -// } -// + + @MainActor + private func widgetsSetConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let params = params as? [[String: String]] else { return nil } + actionsManager.updateWidgetConfigs(with: params) + return nil + } + + @MainActor + private func showContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let params = params as? [String: Any] else { return nil } + actionsManager.showContextMenu(with: params) + return nil + } + private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [String: String] else { return nil } actionsManager.reportException(with: params) diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index ba9b91dbf9..7ee846271c 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -30,9 +30,7 @@ extension SpecialPagesUserScript { @MainActor func withNewTabPage() { - let actionsManager = buildNewTabPageActionsManager() - let userScript = NewTabPageUserScript(actionsManager: actionsManager) - self.registerSubfeature(delegate: userScript) + self.registerSubfeature(delegate: NSApp.delegateTyped.newTabPageUserScript) } func withDuckPlayerIfAvailable() { diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index 38f5f7bc22..c53f1f4ada 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -227,6 +227,10 @@ extension TabExtensionsBuilder { OnboardingTabExtension() } + add { + NewTabPageTabExtension(scriptsPublisher: userScripts.compactMap { $0 }, webViewPublisher: args.webViewFuture) + } + if let tunnelController = dependencies.tunnelController { add { NetworkProtectionControllerTabExtension(tunnelController: tunnelController) diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 71f1885116..862f15d0d9 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -75,7 +75,7 @@ final class UserScripts: UserScriptsProvider { languageCode: lenguageCode) onboardingUserScript = OnboardingUserScript(onboardingActionsManager: sourceProvider.onboardingActionsManager!) - newTabPageUserScript = NewTabPageUserScript(actionsManager: sourceProvider.newTabPageActionsManager!) + newTabPageUserScript = NSApp.delegateTyped.newTabPageUserScript specialPages = SpecialPagesUserScript() diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index aac4abfb3f..7c9d39f747 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -127,7 +127,7 @@ private extension DuckURLSchemeHandler { directoryURL = URL(fileURLWithPath: "/pages/onboarding") } else if url.isReleaseNotesScheme { directoryURL = URL(fileURLWithPath: "/pages/release-notes") - } else if url.isNewTab { + } else if url.isNewTabPage { directoryURL = URL(fileURLWithPath: "/pages/new-tab") } else { assertionFailure("Unknown scheme") @@ -223,7 +223,7 @@ extension URL { return .phishingErrorPage } else if self.isReleaseNotesScheme { return .releaseNotes - } else if self.isNewTab { + } else if self.isNewTabPage { return .newTab } else { return nil @@ -234,7 +234,7 @@ extension URL { return isDuckURLScheme && host == "onboarding" } - var isNewTab: Bool { + var isNewTabPage: Bool { return isDuckURLScheme && host == "newtab" } From 803a9f0338281740454bbe3514d4eb97306102e5 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 6 Nov 2024 18:50:57 +0100 Subject: [PATCH 04/42] Enable developer tools on HTML NTP --- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index b97665c7ec..bef11797a8 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -1137,7 +1137,7 @@ extension MainViewController: NSMenuItemValidation { case #selector(MainViewController.openJavaScriptConsole(_:)), #selector(MainViewController.showPageSource(_:)), #selector(MainViewController.showPageResources(_:)): - return activeTabViewModel?.canReload == true + return activeTabViewModel?.canReload == true || (activeTabViewModel?.tab.url?.isNewTabPage == true && NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled) case #selector(MainViewController.toggleDownloads(_:)): let isDownloadsPopoverShown = self.navigationBarViewController.isDownloadsPopoverShown From ee77f47b8c073415864b8eb85823fd84c33a8e79 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 6 Nov 2024 18:51:36 +0100 Subject: [PATCH 05/42] PoC RMF support --- DuckDuckGo/Application/AppDelegate.swift | 7 +- .../HomePage/NewTabPageActionsManager.swift | 23 ++++- .../HomePage/NewTabPageUserScript.swift | 88 ++++++++++++++++++- .../SpecialPagesUserScriptExtension.swift | 5 -- 4 files changed, 111 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 2ea5b201a2..b1935a87a1 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -44,8 +44,6 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { - let newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared) - private(set) lazy var newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) let experimentalFeatures = ExperimentalFeatures() @objc func toggleHTMLNTP(_ sender: NSMenuItem) { @@ -98,6 +96,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? + let newTabPageActionsManager: NewTabPageActionsManaging + let newTabPageUserScript: NewTabPageUserScript let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! @@ -305,6 +305,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) + + newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared, activeRemoteMessageModel: activeRemoteMessageModel) + newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index 0ac75964fd..efaa16dafb 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -19,6 +19,7 @@ import Foundation import Combine import PixelKit +import RemoteMessaging import Common import os.log @@ -30,6 +31,7 @@ protocol NewTabPageActionsManaging: AnyObject { func reportException(with params: [String: String]) func showContextMenu(with params: [String: Any]) func updateWidgetConfigs(with params: [[String: String]]) + func getRemoteMessage() -> NTP.RMFMessage? } struct NewTabPageConfiguration: Encodable { @@ -70,11 +72,14 @@ struct NewTabPageConfiguration: Encodable { final class NewTabPageActionsManager: NewTabPageActionsManaging { private let appearancePreferences: AppearancePreferences + private let activeRemoteMessageModel: ActiveRemoteMessageModel + private var cancellables = Set() weak var userScript: NewTabPageUserScript? - init(appearancePreferences: AppearancePreferences) { + init(appearancePreferences: AppearancePreferences, activeRemoteMessageModel: ActiveRemoteMessageModel) { self.appearancePreferences = appearancePreferences + self.activeRemoteMessageModel = activeRemoteMessageModel appearancePreferences.$isFavoriteVisible.dropFirst().removeDuplicates().asVoid() .receive(on: DispatchQueue.main) @@ -89,15 +94,25 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { self?.notifyWidgetConfigsDidChange() } .store(in: &cancellables) + + activeRemoteMessageModel.$remoteMessage.dropFirst() + .sink { [weak self] remoteMessage in + self?.notifyRemoteMessageDidChange(remoteMessage) + } + .store(in: &cancellables) } private func notifyWidgetConfigsDidChange() { - userScript?.widgetConfigsUpdated(widgetConfigs: [ + userScript?.notifyWidgetConfigsDidChange(widgetConfigs: [ .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) ]) } + private func notifyRemoteMessageDidChange(_ remoteMessage: RemoteMessageModel?) { + userScript?.notifyRemoteMessageDidChange(.small(.init(descriptionText: "Hello, this is a description", id: "hejka", titleText: "Hello I'm a title"))) + } + var configuration: NewTabPageConfiguration { #if DEBUG || REVIEW let env = "development" @@ -179,6 +194,10 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { } } + func getRemoteMessage() -> NTP.RMFMessage? { + .small(.init(descriptionText: "Hello, this is a description", id: "hejka", titleText: "Hello I'm a title")) + } + func reportException(with params: [String: String]) { let message = params["message"] ?? "" let id = params["id"] ?? "" diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index a1429dc909..b6619234e8 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -34,6 +34,7 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { case initialSetup case reportInitException case reportPageException + case rmfGetData = "rmf_getData" case widgetsSetConfig = "widgets_setConfig" } @@ -52,6 +53,7 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { .initialSetup: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, .reportInitException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, .reportPageException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, + .rmfGetData: { [weak self] in try await self?.rmfGetData(params: $0, original: $1) }, .widgetsSetConfig: { [weak self] in try await self?.widgetsSetConfig(params: $0, original: $1) } ] @@ -61,17 +63,24 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { return methodHandlers[messageName] } - func widgetConfigsUpdated(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { + func notifyWidgetConfigsDidChange(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { for webView in webViews.allObjects { broker?.push(method: "widgets_onConfigUpdated", params: widgetConfigs, for: self, into: webView) } } + + func notifyRemoteMessageDidChange(_ remoteMessage: NTP.RMFMessage?) { + let data = NTP.RMFData(content: remoteMessage) + for webView in webViews.allObjects { + broker?.push(method: "rmf_onDataUpdate", params: data, for: self, into: webView) + } + } } extension NewTabPageUserScript { @MainActor private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { - return actionsManager.configuration + actionsManager.configuration } @MainActor @@ -88,12 +97,85 @@ extension NewTabPageUserScript { return nil } + @MainActor + private func rmfGetData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + let data = NTP.RMFData(content: actionsManager.getRemoteMessage()) + return data + } + private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [String: String] else { return nil } actionsManager.reportException(with: params) return nil } - struct Result: Encodable {} +} +enum NTP { + struct RMFData: Encodable { + var content: RMFMessage? + } + + enum RMFMessage: Encodable { + case small(SmallMessage), medium(MediumMessage), bigSingleAction(BigSingleActionMessage), bigTwoAction(BigTwoActionMessage) + + func encode(to encoder: any Encoder) throws { + try message.encode(to: encoder) + } + + var message: Encodable { + switch self { + case .small(let message): + return message + case .medium(let message): + return message + case .bigSingleAction(let message): + return message + case .bigTwoAction(let message): + return message + } + } + } + + struct SmallMessage: Encodable { + let messageType = "small" + + var descriptionText: String + var id: String + var titleText: String + } + + struct MediumMessage: Encodable { + let messageType = "medium" + + var descriptionText: String + var icon: RMFIcon + var id: String + var titleText: String + } + + struct BigSingleActionMessage: Encodable { + let messageType = "big_single_action" + + var descriptionText: String + var icon: RMFIcon + var id: String + var primaryActionText: String + var titleText: String + } + + struct BigTwoActionMessage: Encodable { + let messageType = "big_two_action" + + var descriptionText: String + var icon: RMFIcon + var id: String + var primaryActionText: String + var secondaryActionText: String + var titleText: String + } + + enum RMFIcon: String, Encodable { + case announce, ddgAnnounce, criticalUpdate, appUpdate, privacyPro + } } diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index 7ee846271c..c9f7f42b8b 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -67,9 +67,4 @@ extension SpecialPagesUserScript { appearancePreferences: AppearancePreferences.shared, startupPreferences: StartupPreferences.shared) } - - @MainActor - private func buildNewTabPageActionsManager() -> NewTabPageActionsManaging { - NewTabPageActionsManager(appearancePreferences: .shared) - } } From d70a446a7e44701b23429eba3ff52ef0ad1f07ad Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 17:58:37 +0100 Subject: [PATCH 06/42] Use a separate web view for NTP --- .../Tab/View/BrowserTabViewController.swift | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 2d8feb22dc..3d4aef94d3 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -27,6 +27,7 @@ import PixelKit import os.log import Onboarding import Freemium +import UserScript protocol BrowserTabViewControllerDelegate: AnyObject { func highlightFireButton() @@ -34,12 +35,32 @@ protocol BrowserTabViewControllerDelegate: AnyObject { func dismissViewHighlight() } +extension BrowserTabViewController: UserContentControllerDelegate { + func userContentController( + _ userContentController: UserContentController, + didInstallContentRuleLists contentRuleLists: [String: WKContentRuleList], + userScripts: any UserScriptsProvider, + updateEvent: ContentBlockerRulesManager.UpdateEvent + ) {} +} + final class BrowserTabViewController: NSViewController { private lazy var browserTabView = BrowserTabView(frame: .zero, backgroundColor: .browserTabBackground) private lazy var hoverLabel = NSTextField(string: URL.duckDuckGo.absoluteString) private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) + private lazy var newTabPageWebView: WebView = { + let configuration = WKWebViewConfiguration() + configuration.applyStandardConfiguration(contentBlocking: PrivacyFeatures.contentBlocking, + burnerMode: tabCollectionViewModel.burnerMode, + earlyAccessHandlers: []) + let userContentController = configuration.userContentController as? UserContentController + userContentController?.delegate = self + + + return WebView(frame: .zero, configuration: configuration) + }() private weak var webView: WebView? private weak var webViewContainer: NSView? private weak var webViewSnapshot: NSView? @@ -138,6 +159,7 @@ final class BrowserTabViewController: NSViewController { } view.registerForDraggedTypes([.URL, .fileURL]) + newTabPageWebView.load(URLRequest(url: URL.newtab)) } @objc func windowDidBecomeActive(notification: Notification) { @@ -516,7 +538,7 @@ final class BrowserTabViewController: NSViewController { } func displayWebView(of tabViewModel: TabViewModel) { - let newWebView = tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTab == true ? newTabPageWebView : tabViewModel.tab.webView cleanUpRemoteWebViewIfNeeded(newWebView) webView = newWebView @@ -824,11 +846,13 @@ final class BrowserTabViewController: NSViewController { return false } + let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTab == true ? newTabPageWebView : tabViewModel.tab.webView + let isPinnedTab = tabCollectionViewModel.pinnedTabsCollection?.tabs.contains(tabViewModel.tab) == true let isKeyWindow = view.window?.isKeyWindow == true let tabIsNotOnScreen = webView?.tabContentView.superview == nil - let isDifferentTabDisplayed = webView !== tabViewModel.tab.webView + let isDifferentTabDisplayed = webView !== newWebView return isDifferentTabDisplayed || tabIsNotOnScreen @@ -1434,7 +1458,7 @@ extension BrowserTabViewController { guard let self, self.tabViewModel === tabViewModel else { return } - // only make web view first responder after replacing the + // only make web view first responder after replacing the // snapshot if the address bar is not the first responder if view.window?.firstResponder === view.window { viewToMakeFirstResponderAfterAdding = { [weak self] in From 2a33e6438cb90e403495edcaf5cbb37879538861 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 23:30:12 +0100 Subject: [PATCH 07/42] Set up NTP user script in a dedicated webView --- DuckDuckGo.xcodeproj/project.pbxproj | 12 +-- .../HomePage/NewTabPageTabExtension.swift | 59 ------------ .../NewTabPageUserContentController.swift | 90 +++++++++++++++++++ .../SpecialPagesUserScriptExtension.swift | 1 - .../Tab/TabExtensions/TabExtensions.swift | 4 - DuckDuckGo/Tab/UserScripts/UserScripts.swift | 5 -- .../Tab/View/BrowserTabViewController.swift | 24 ++--- 7 files changed, 103 insertions(+), 92 deletions(-) delete mode 100644 DuckDuckGo/HomePage/NewTabPageTabExtension.swift create mode 100644 DuckDuckGo/HomePage/NewTabPageUserContentController.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e369144a6b..4245e3e471 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1100,6 +1100,8 @@ 372A0FED2B2379310033BF7F /* SyncMetricsEventsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372A0FEB2B2379310033BF7F /* SyncMetricsEventsHandler.swift */; }; 372BC2A12A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */; }; 372BC2A22A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */; }; + 372ED7C22CDD481B002287EC /* NewTabPageUserContentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */; }; + 372ED7C32CDD481B002287EC /* NewTabPageUserContentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */; }; 3739326529AE4B39009346AE /* DDGSync in Frameworks */ = {isa = PBXBuildFile; productRef = 3739326429AE4B39009346AE /* DDGSync */; }; 3739326729AE4B42009346AE /* DDGSync in Frameworks */ = {isa = PBXBuildFile; productRef = 3739326629AE4B42009346AE /* DDGSync */; }; 373A1AA8283ED1B900586521 /* BookmarkHTMLReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373A1AA7283ED1B900586521 /* BookmarkHTMLReader.swift */; }; @@ -1267,8 +1269,6 @@ 37FB430F2CDB84A500479A1E /* NewTabPageUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */; }; 37FB43112CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */; }; 37FB43122CDB883B00479A1E /* NewTabPageActionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */; }; - 37FB43142CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */; }; - 37FB43152CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */; }; 37FD78112A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; 4B0135CE2729F1AA00D54834 /* NSPasteboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */; }; @@ -3524,6 +3524,7 @@ 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperimentTests.swift; sourceTree = ""; }; 372A0FEB2B2379310033BF7F /* SyncMetricsEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMetricsEventsHandler.swift; sourceTree = ""; }; 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCredentialsAdapter.swift; sourceTree = ""; }; + 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageUserContentController.swift; sourceTree = ""; }; 373A1AA7283ED1B900586521 /* BookmarkHTMLReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkHTMLReader.swift; sourceTree = ""; }; 373A1AA9283ED86C00586521 /* BookmarksHTMLReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksHTMLReaderTests.swift; sourceTree = ""; }; 373A1AAF2842C4EA00586521 /* BookmarkHTMLImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkHTMLImporter.swift; sourceTree = ""; }; @@ -3651,7 +3652,6 @@ 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayer.swift; sourceTree = ""; }; 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageUserScript.swift; sourceTree = ""; }; 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageActionsManager.swift; sourceTree = ""; }; - 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageTabExtension.swift; sourceTree = ""; }; 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; 4B02197F25E05FAC00ED7DEA /* FireproofingURLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofingURLExtensions.swift; sourceTree = ""; }; @@ -8626,8 +8626,8 @@ AAE71DB225F66A0900D74437 /* HomePage */ = { isa = PBXGroup; children = ( - 37FB43132CDBA20500479A1E /* NewTabPageTabExtension.swift */, 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */, + 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */, 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */, 85589E8527BBB8DD0038AD11 /* Model */, AAE71DB325F66A3F00D74437 /* View */, @@ -11187,6 +11187,7 @@ 4B9DB0362A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */, 37197EA82942443D00394917 /* BrowserTabViewController.swift in Sources */, 3706FB39293F65D500E42796 /* PrivacyDashboardPopover.swift in Sources */, + 372ED7C32CDD481B002287EC /* NewTabPageUserContentController.swift in Sources */, 3706FB3B293F65D500E42796 /* RootView.swift in Sources */, 3706FB3C293F65D500E42796 /* AddressBarTextField.swift in Sources */, 3706FB3D293F65D500E42796 /* FocusRingView.swift in Sources */, @@ -11814,7 +11815,6 @@ 3706FCA0293F65D500E42796 /* ContiguousBytesExtension.swift in Sources */, B602E8172A1E2570006D261F /* URL+NetworkProtection.swift in Sources */, 3706FCA1293F65D500E42796 /* AdjacentItemEnumerator.swift in Sources */, - 37FB43142CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */, 9F9C49FA2BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, 31ECDA122BED339600AE679F /* DataBrokerAuthenticationManagerBuilder.swift in Sources */, 3706FCA2293F65D500E42796 /* ChromiumKeychainPrompt.swift in Sources */, @@ -12966,6 +12966,7 @@ 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, B6B5F5842B03580A008DB58A /* RequestFilePermissionView.swift in Sources */, 4B1E6EEE27AB5E5100F51793 /* PasswordManagementListSection.swift in Sources */, + 372ED7C22CDD481B002287EC /* NewTabPageUserContentController.swift in Sources */, 31C9ADE52AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */, AA222CB92760F74E00321475 /* FaviconReferenceCache.swift in Sources */, 4B9292A126670D2A00AD2C21 /* BookmarkTreeController.swift in Sources */, @@ -13138,7 +13139,6 @@ 4BE65479271FCD41008D1D63 /* EditableTextView.swift in Sources */, AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */, 1DA84D322C119AE70011C80F /* UpdateMenuItemFactory.swift in Sources */, - 37FB43152CDBA20900479A1E /* NewTabPageTabExtension.swift in Sources */, B6B4D1CF2B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, B688B4DA273E6D3B0087BEAF /* MainView.swift in Sources */, B61E2CD5294346C000773D8A /* Tab+Navigation.swift in Sources */, diff --git a/DuckDuckGo/HomePage/NewTabPageTabExtension.swift b/DuckDuckGo/HomePage/NewTabPageTabExtension.swift deleted file mode 100644 index be60b2a89c..0000000000 --- a/DuckDuckGo/HomePage/NewTabPageTabExtension.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// NewTabPageTabExtension.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 -import Foundation -import Navigation - -protocol NewTabPageUserScriptProvider { - var newTabPageUserScript: NewTabPageUserScript? { get } -} -extension UserScripts: NewTabPageUserScriptProvider {} - -final class NewTabPageTabExtension: NavigationResponder { - - private weak var webView: WKWebView? { - didSet { - newTabPageUserScript?.webViews.add(webView) - } - } - private weak var newTabPageUserScript: NewTabPageUserScript? - private var cancellables: Set = [] - - init(scriptsPublisher: some Publisher, webViewPublisher: some Publisher) { - webViewPublisher.sink { [weak self] webView in - self?.webView = webView - }.store(in: &cancellables) - - scriptsPublisher.sink { [weak self] scripts in - guard let self else { return } - newTabPageUserScript = scripts.newTabPageUserScript - newTabPageUserScript?.webViews.add(webView) - }.store(in: &cancellables) - } -} - -protocol NewTabPageTabExtensionProtocol: AnyObject, NavigationResponder {} - -extension NewTabPageTabExtension: NewTabPageTabExtensionProtocol, TabExtension { - func getPublicProtocol() -> NewTabPageTabExtensionProtocol { self } -} - -extension TabExtensions { - var newTabPage: NewTabPageTabExtensionProtocol? { resolve(NewTabPageTabExtension.self) } -} diff --git a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift new file mode 100644 index 0000000000..cb4647b7be --- /dev/null +++ b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift @@ -0,0 +1,90 @@ +// +// NewTabPageUserContentController.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 Foundation +import WebKit +import BrowserServicesKit +import UserScript + +final class NewTabPageUserContentController: WKUserContentController { + + let ntpUserScripts: NTPUserScript + + @MainActor + override init() { + ntpUserScripts = NTPUserScript() + + super.init() + + ntpUserScripts.userScripts.forEach { + let userScript = $0.makeWKUserScriptSync() + self.installUserScripts([userScript], handlers: [$0]) + } + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @MainActor + private func installUserScripts(_ wkUserScripts: [WKUserScript], handlers: [UserScript]) { + handlers.forEach { self.addHandler($0) } + wkUserScripts.forEach(self.addUserScript) + } +} + +@MainActor +final class NTPUserScript: UserScriptsProvider { + lazy var userScripts: [UserScript] = [specialPagesUserScriptIsolated] + + let specialPagesUserScriptIsolated: SpecialPagesUserScript + + init() { + specialPagesUserScriptIsolated = SpecialPagesUserScript() + specialPagesUserScriptIsolated.withNewTabPage() + } + + @MainActor + func loadWKUserScripts() async -> [WKUserScript] { + return await withTaskGroup(of: WKUserScriptBox.self) { @MainActor group in + var wkUserScripts = [WKUserScript]() + userScripts.forEach { userScript in + group.addTask { @MainActor in + await userScript.makeWKUserScript() + } + } + for await result in group { + wkUserScripts.append(result.wkUserScript) + } + + return wkUserScripts + } + } +} + +extension WKWebViewConfiguration { + + @MainActor + func applyNTPConfiguration() { + preferences.isFraudulentWebsiteWarningEnabled = false + if urlSchemeHandler(forURLScheme: URL.NavigationalScheme.duck.rawValue) == nil { + setURLSchemeHandler(DuckURLSchemeHandler(), forURLScheme: URL.NavigationalScheme.duck.rawValue) + } + self.userContentController = NewTabPageUserContentController() + } +} diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index c9f7f42b8b..6c3ead0f1b 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -53,7 +53,6 @@ extension SpecialPagesUserScript { @MainActor func withAllSubfeatures() { withOnboarding() - withNewTabPage() withErrorPages() withDuckPlayerIfAvailable() } diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift index c53f1f4ada..38f5f7bc22 100644 --- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift +++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift @@ -227,10 +227,6 @@ extension TabExtensionsBuilder { OnboardingTabExtension() } - add { - NewTabPageTabExtension(scriptsPublisher: userScripts.compactMap { $0 }, webViewPublisher: args.webViewFuture) - } - if let tunnelController = dependencies.tunnelController { add { NetworkProtectionControllerTabExtension(tunnelController: tunnelController) diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 862f15d0d9..3c0e9fa0c8 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -47,7 +47,6 @@ final class UserScripts: UserScriptsProvider { let youtubePlayerUserScript: YoutubePlayerUserScript? let specialErrorPageUserScript: SpecialErrorPageUserScript? let onboardingUserScript: OnboardingUserScript? - let newTabPageUserScript: NewTabPageUserScript? #if SPARKLE let releaseNotesUserScript: ReleaseNotesUserScript? #endif @@ -75,7 +74,6 @@ final class UserScripts: UserScriptsProvider { languageCode: lenguageCode) onboardingUserScript = OnboardingUserScript(onboardingActionsManager: sourceProvider.onboardingActionsManager!) - newTabPageUserScript = NSApp.delegateTyped.newTabPageUserScript specialPages = SpecialPagesUserScript() @@ -115,9 +113,6 @@ final class UserScripts: UserScriptsProvider { specialPages.registerSubfeature(delegate: onboardingUserScript) } - if let newTabPageUserScript { - specialPages.registerSubfeature(delegate: newTabPageUserScript) - } userScripts.append(specialPages) } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 3d4aef94d3..5145b19bd9 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -35,15 +35,6 @@ protocol BrowserTabViewControllerDelegate: AnyObject { func dismissViewHighlight() } -extension BrowserTabViewController: UserContentControllerDelegate { - func userContentController( - _ userContentController: UserContentController, - didInstallContentRuleLists contentRuleLists: [String: WKContentRuleList], - userScripts: any UserScriptsProvider, - updateEvent: ContentBlockerRulesManager.UpdateEvent - ) {} -} - final class BrowserTabViewController: NSViewController { private lazy var browserTabView = BrowserTabView(frame: .zero, backgroundColor: .browserTabBackground) @@ -52,14 +43,13 @@ final class BrowserTabViewController: NSViewController { private lazy var newTabPageWebView: WebView = { let configuration = WKWebViewConfiguration() - configuration.applyStandardConfiguration(contentBlocking: PrivacyFeatures.contentBlocking, - burnerMode: tabCollectionViewModel.burnerMode, - earlyAccessHandlers: []) - let userContentController = configuration.userContentController as? UserContentController - userContentController?.delegate = self + configuration.applyNTPConfiguration() + + let webView = WebView(frame: .zero, configuration: configuration) + NSApp.delegateTyped.newTabPageUserScript.webViews.add(webView) - return WebView(frame: .zero, configuration: configuration) + return webView }() private weak var webView: WebView? private weak var webViewContainer: NSView? @@ -538,7 +528,7 @@ final class BrowserTabViewController: NSViewController { } func displayWebView(of tabViewModel: TabViewModel) { - let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTab == true ? newTabPageWebView : tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTabPage == true ? newTabPageWebView : tabViewModel.tab.webView cleanUpRemoteWebViewIfNeeded(newWebView) webView = newWebView @@ -846,7 +836,7 @@ final class BrowserTabViewController: NSViewController { return false } - let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTab == true ? newTabPageWebView : tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTabPage == true ? newTabPageWebView : tabViewModel.tab.webView let isPinnedTab = tabCollectionViewModel.pinnedTabsCollection?.tabs.contains(tabViewModel.tab) == true let isKeyWindow = view.window?.isKeyWindow == true From b546100c9f75ac43d006e5ee2574bb1f6d39b6e6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 31 Oct 2024 11:25:55 +0100 Subject: [PATCH 08/42] Define experimental feature and enable switching between HTML and native NTP --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++ DuckDuckGo/Application/AppDelegate.swift | 5 ++ .../Application/ExperimentalFeatures.swift | 70 +++++++++++++++++++ .../Utilities/UserDefaultsWrapper.swift | 4 ++ DuckDuckGo/Menus/MainMenu.swift | 13 ++++ .../Tab/View/BrowserTabViewController.swift | 17 ++++- .../Updates/ReleaseNotesUserScript.swift | 2 +- .../YoutubePlayer/DuckURLSchemeHandler.swift | 11 ++- 8 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 DuckDuckGo/Application/ExperimentalFeatures.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f0825b974c..f4e84b80d8 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1194,6 +1194,8 @@ 37A6A8F22AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */; }; 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */; }; 37A6A8F72AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */; }; + 37A746A62CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */; }; + 37A746A72CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */; }; 37A803DB27FD69D300052F4C /* DataImportResources in Resources */ = {isa = PBXBuildFile; fileRef = 37A803DA27FD69D300052F4C /* DataImportResources */; }; 37AAA41C2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */; }; 37AAA41D2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */; }; @@ -3629,6 +3631,7 @@ 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartupPreferences.swift; sourceTree = ""; }; 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboarding.swift; sourceTree = ""; }; 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboardingViewController.swift; sourceTree = ""; }; + 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeatures.swift; sourceTree = ""; }; 37A803DA27FD69D300052F4C /* DataImportResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DataImportResources; sourceTree = ""; }; 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarTextFieldView.swift; sourceTree = ""; }; 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; @@ -7644,6 +7647,7 @@ AA4D700525545EDE00C3411E /* Application */ = { isa = PBXGroup; children = ( + 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */, 4B4D60E12A0C883A00BCD287 /* AppMain.swift */, B62B48382ADE46FC000DECE5 /* Application.swift */, AA585D81248FD31100E9A3E2 /* AppDelegate.swift */, @@ -11048,6 +11052,7 @@ 3706FA9F293F65D500E42796 /* FeedbackPresenter.swift in Sources */, CD2AB5C42C8222F70019EB49 /* PhishingDetectionStateManager.swift in Sources */, 37A6A8F22AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */, + 37A746A72CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */, 859F30652A72A9FA00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 37197EA22942441900394917 /* Tab+Dialogs.swift in Sources */, 3706FAA0293F65D500E42796 /* UserAgent.swift in Sources */, @@ -13214,6 +13219,7 @@ B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */, B68458C525C7EA0C00DC17B6 /* TabCollection+NSSecureCoding.swift in Sources */, + 37A746A62CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */, C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 370C23072C76A36500A80A3E /* BackgroundPickerView.swift in Sources */, 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index b9212e03e7..a35c1caa8d 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -44,6 +44,11 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { + let experimentalFeatures = ExperimentalFeatures() + @objc func toggleHTMLNTP(_ sender: NSMenuItem) { + experimentalFeatures.isHTMLNewTabPageEnabled.toggle() + } + #if DEBUG let disableCVDisplayLinkLogs: Void = { // Disable CVDisplayLink logs diff --git a/DuckDuckGo/Application/ExperimentalFeatures.swift b/DuckDuckGo/Application/ExperimentalFeatures.swift new file mode 100644 index 0000000000..29d95b145c --- /dev/null +++ b/DuckDuckGo/Application/ExperimentalFeatures.swift @@ -0,0 +1,70 @@ +// +// ExperimentalFeatures.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 Foundation + +protocol ExperimentalFeaturesPersistor { + var isHTMLNewTabPageEnabled: Bool { get set } +} + +struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor { + @UserDefaultsWrapper(key: .htmlNewTabPage, defaultValue: false) + var isHTMLNewTabPageEnabled: Bool +} + +protocol ExperimentalFeaturesHandler { + func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) +} + +struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { + func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) { + Task { @MainActor in + WindowControllersManager.shared.mainWindowControllers.forEach { mainWindowController in + if mainWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab { + mainWindowController.mainViewController.browserTabViewController.refreshTab() + } + } + } + } +} + +final class ExperimentalFeatures { + + private var persistor: ExperimentalFeaturesPersistor + private var actionHandler: ExperimentalFeaturesHandler + + init( + persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(), + actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() + ) { + self.persistor = persistor + self.actionHandler = actionHandler + } + + var isHTMLNewTabPageEnabled: Bool { + get { + persistor.isHTMLNewTabPageEnabled + } + set { + if newValue != persistor.isHTMLNewTabPageEnabled { + persistor.isHTMLNewTabPageEnabled = newValue + actionHandler.isHTMLNewTabPageEnabledDidChange(newValue) + } + } + } +} diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index a18850b028..56c3adc634 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -224,6 +224,10 @@ public struct UserDefaultsWrapper { // Subscription case subscriptionEnvironment = "subscription.environment" + + // Experimental Feature Flags + + case htmlNewTabPage = "experimental.html-new-tab-page" } enum RemovedKeys: String, CaseIterable { diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index ce940baf61..f97c1a3a20 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -91,6 +91,7 @@ final class MainMenu: NSMenu { let windowsMenu = NSMenu(title: UserText.mainMenuWindow) // MARK: Debug + private var experimentalFeaturesMenu: NSMenu? private var loggingMenu: NSMenu? let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil) @@ -446,6 +447,11 @@ final class MainMenu: NSMenu { updateInternalUserItem() updateRemoteConfigurationInfo() updateAutofillDebugScriptMenuItem() + updateExperimentalFeatures() + } + + private func updateExperimentalFeatures() { + experimentalFeaturesMenu?.items[0].state = NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled ? .on : .off } // MARK: - Bookmarks @@ -617,7 +623,14 @@ final class MainMenu: NSMenu { @MainActor private func setupDebugMenu() -> NSMenu { + let experimentalFeaturesMenu = NSMenu() { + NSMenuItem(title: "HTML New Tab Page", action: #selector(AppDelegate.toggleHTMLNTP(_:)), representedObject: 10) + } + self.experimentalFeaturesMenu = experimentalFeaturesMenu let debugMenu = NSMenu(title: "Debug") { + NSMenuItem(title: "Experimental features") + .submenu(experimentalFeaturesMenu) + NSMenuItem.separator() NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() NSMenuItem(title: "Tab") { diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 46667bc167..2d8feb22dc 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -777,8 +777,12 @@ final class BrowserTabViewController: NSViewController { updateTabIfNeeded(tabViewModel: tabViewModel) case .newtab: - removeAllTabContent() - addAndLayoutChild(homePageViewControllerCreatingIfNeeded()) + if NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled { + updateTabIfNeeded(tabViewModel: tabViewModel) + } else { + removeAllTabContent() + addAndLayoutChild(homePageViewControllerCreatingIfNeeded()) + } case .dataBrokerProtection: removeAllTabContent() @@ -790,6 +794,11 @@ final class BrowserTabViewController: NSViewController { } } + func refreshTab() { + guard let tabViewModel else { return } + showTabContent(of: tabViewModel) + } + func updateTabIfNeeded(tabViewModel: TabViewModel?) { if shouldReplaceWebView(for: tabViewModel) { removeAllTabContent(includingWebView: true) @@ -835,7 +844,9 @@ final class BrowserTabViewController: NSViewController { switch tabViewModel.tab.content { case .onboarding: return - case .newtab, .settings: + case .newtab: + containsHostingView = !NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled + case .settings: containsHostingView = true default: containsHostingView = false diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 620c2ff3b9..16e7dc7e02 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -26,7 +26,7 @@ import Combine final class ReleaseNotesUserScript: NSObject, Subfeature { lazy var updateController: UpdateControllerProtocol = Application.appDelegate.updateController - var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes")]) + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes"), .exact(hostname: "newtab")]) let featureName: String = "release-notes" weak var broker: UserScriptMessageBroker? weak var webView: WKWebView? { diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index e169c255e5..aac4abfb3f 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -30,7 +30,7 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { } switch requestURL.type { - case .onboarding, .releaseNotes: + case .onboarding, .releaseNotes, .newTab: handleSpecialPages(urlSchemeTask: urlSchemeTask) case .duckPlayer: handleDuckPlayer(requestURL: requestURL, urlSchemeTask: urlSchemeTask, webView: webView) @@ -127,6 +127,8 @@ private extension DuckURLSchemeHandler { directoryURL = URL(fileURLWithPath: "/pages/onboarding") } else if url.isReleaseNotesScheme { directoryURL = URL(fileURLWithPath: "/pages/release-notes") + } else if url.isNewTab { + directoryURL = URL(fileURLWithPath: "/pages/new-tab") } else { assertionFailure("Unknown scheme") return nil @@ -205,6 +207,7 @@ private extension DuckURLSchemeHandler { extension URL { enum URLType { + case newTab case onboarding case duckPlayer case releaseNotes @@ -220,6 +223,8 @@ extension URL { return .phishingErrorPage } else if self.isReleaseNotesScheme { return .releaseNotes + } else if self.isNewTab { + return .newTab } else { return nil } @@ -229,6 +234,10 @@ extension URL { return isDuckURLScheme && host == "onboarding" } + var isNewTab: Bool { + return isDuckURLScheme && host == "newtab" + } + var isDuckURLScheme: Bool { navigationalScheme == .duck } From 7717a8fb4b1d7e70e58c6e8c96504b0ff2c33749 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 13:30:39 +0100 Subject: [PATCH 09/42] Extract Experimental Features menu into its own class --- DuckDuckGo.xcodeproj/project.pbxproj | 26 +++++-- .../Application/ExperimentalFeatures.swift | 70 ------------------- .../ExperimentalFeaturesMenu.swift | 48 +++++++++++++ DuckDuckGo/Menus/MainMenu.swift | 11 +-- 4 files changed, 69 insertions(+), 86 deletions(-) delete mode 100644 DuckDuckGo/Application/ExperimentalFeatures.swift create mode 100644 DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f4e84b80d8..9dc2b105eb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1058,6 +1058,10 @@ 370C230F2C76A3D600A80A3E /* SettingsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23042C76A31F00A80A3E /* SettingsGrid.swift */; }; 370C23112C7747E200A80A3E /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23102C7747E200A80A3E /* ImageProcessor.swift */; }; 370C23122C7747E200A80A3E /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23102C7747E200A80A3E /* ImageProcessor.swift */; }; + 37115A0F2CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */; }; + 37115A102CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */; }; + 37115A142CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */; }; + 37115A152CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */; }; 371209212C232E3F003ADF3D /* RemoteMessagingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */; }; 371209232C232E66003ADF3D /* RemoteMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 371209222C232E66003ADF3D /* RemoteMessaging */; }; 371209252C232E6C003ADF3D /* RemoteMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 371209242C232E6C003ADF3D /* RemoteMessaging */; }; @@ -1194,8 +1198,6 @@ 37A6A8F22AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */; }; 37A6A8F62AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */; }; 37A6A8F72AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */; }; - 37A746A62CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */; }; - 37A746A72CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */; }; 37A803DB27FD69D300052F4C /* DataImportResources in Resources */ = {isa = PBXBuildFile; fileRef = 37A803DA27FD69D300052F4C /* DataImportResources */; }; 37AAA41C2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */; }; 37AAA41D2C9CB9C0002A5377 /* AddressBarTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */; }; @@ -3549,6 +3551,8 @@ 370C23082C76A39900A80A3E /* BackgroundCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundCategoryView.swift; sourceTree = ""; }; 370C230A2C76A3BC00A80A3E /* BackgroundThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThumbnailView.swift; sourceTree = ""; }; 370C23102C7747E200A80A3E /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = ""; }; + 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesMenu.swift; sourceTree = ""; }; + 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeatures.swift; sourceTree = ""; }; 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingClient.swift; sourceTree = ""; }; 371209292C2333A0003ADF3D /* RemoteMessagingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingDatabase.swift; sourceTree = ""; }; 3712092B2C23383C003ADF3D /* RemoteMessagingStoreErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingStoreErrorHandling.swift; sourceTree = ""; }; @@ -3631,7 +3635,6 @@ 37A4CEB9282E992F00D75B89 /* StartupPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StartupPreferences.swift; sourceTree = ""; }; 37A6A8F02AFCC988008580A3 /* FaviconsFetcherOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboarding.swift; sourceTree = ""; }; 37A6A8F52AFCCA59008580A3 /* FaviconsFetcherOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconsFetcherOnboardingViewController.swift; sourceTree = ""; }; - 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeatures.swift; sourceTree = ""; }; 37A803DA27FD69D300052F4C /* DataImportResources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = DataImportResources; sourceTree = ""; }; 37AAA41B2C9CB9C0002A5377 /* AddressBarTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarTextFieldView.swift; sourceTree = ""; }; 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesViewController.swift; sourceTree = ""; }; @@ -5626,6 +5629,15 @@ path = HomePageSettings; sourceTree = ""; }; + 37115A132CDCE65D00F2FF5C /* ExperimentalFeatures */ = { + isa = PBXGroup; + children = ( + 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */, + 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */, + ); + path = ExperimentalFeatures; + sourceTree = ""; + }; 3712091F2C232E2B003ADF3D /* RemoteMessaging */ = { isa = PBXGroup; children = ( @@ -7647,7 +7659,6 @@ AA4D700525545EDE00C3411E /* Application */ = { isa = PBXGroup; children = ( - 37A746A52CDA4C6200C438AB /* ExperimentalFeatures.swift */, 4B4D60E12A0C883A00BCD287 /* AppMain.swift */, B62B48382ADE46FC000DECE5 /* Application.swift */, AA585D81248FD31100E9A3E2 /* AppDelegate.swift */, @@ -7750,6 +7761,7 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 4B65143C26392483005B46EB /* Email */, B68412192B6A16030092F66A /* ErrorPage */, + 37115A132CDCE65D00F2FF5C /* ExperimentalFeatures */, AA5FA695275F823900DCE9C9 /* Favicons */, 1D36E651298A84F600AA485D /* FeatureFlagging */, AA3863C227A1E1C000749AB5 /* Feedback */, @@ -11052,7 +11064,6 @@ 3706FA9F293F65D500E42796 /* FeedbackPresenter.swift in Sources */, CD2AB5C42C8222F70019EB49 /* PhishingDetectionStateManager.swift in Sources */, 37A6A8F22AFCC988008580A3 /* FaviconsFetcherOnboarding.swift in Sources */, - 37A746A72CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */, 859F30652A72A9FA00C20372 /* BookmarksBarPromptPopover.swift in Sources */, 37197EA22942441900394917 /* Tab+Dialogs.swift in Sources */, 3706FAA0293F65D500E42796 /* UserAgent.swift in Sources */, @@ -11233,6 +11244,7 @@ 3706FB13293F65D500E42796 /* Bookmark.swift in Sources */, 3706FB14293F65D500E42796 /* ConnectBitwardenViewModel.swift in Sources */, 3706FB15293F65D500E42796 /* NSNotificationName+DataImport.swift in Sources */, + 37115A152CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */, EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */, 3706FB16293F65D500E42796 /* StoredPermission.swift in Sources */, 3706FB17293F65D500E42796 /* FirePopoverCollectionViewHeader.swift in Sources */, @@ -11402,6 +11414,7 @@ 567A23D82C8871290010F66C /* OnboardingFireButtonDialogViewModel.swift in Sources */, 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */, 1D4B03D62CA4432000224E99 /* BookmarkUrlExtension.swift in Sources */, + 37115A102CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */, 987799F42999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, 3706FB7B293F65D500E42796 /* BookmarkImport.swift in Sources */, @@ -12875,6 +12888,7 @@ 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */, 37CC53F427E8D4620028713D /* NSPathControlView.swift in Sources */, B6BF5D8929470BC4006742B1 /* HTTPSUpgradeTabExtension.swift in Sources */, + 37115A142CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */, 1D36E65B298ACD2900AA485D /* AppIconChanger.swift in Sources */, 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */, 7BCB90C22C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, @@ -12900,6 +12914,7 @@ AAE246F8270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift in Sources */, 314872742CC653D500EEF89B /* AIChatOnboardingPopover.swift in Sources */, AAB7320926DD0CD9002FACF9 /* FireViewController.swift in Sources */, + 37115A0F2CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */, 31C26A0D2CBE9DFE00FFF462 /* AIChatPreferences.swift in Sources */, 4B92928C26670D1700AD2C21 /* OutlineSeparatorViewCell.swift in Sources */, 8426108D2C9811F30070D5F9 /* KeyEquivalentView.swift in Sources */, @@ -13219,7 +13234,6 @@ B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, B68458B025C7E76A00DC17B6 /* WindowManager+StateRestoration.swift in Sources */, B68458C525C7EA0C00DC17B6 /* TabCollection+NSSecureCoding.swift in Sources */, - 37A746A62CDA4C6600C438AB /* ExperimentalFeatures.swift in Sources */, C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */, 370C23072C76A36500A80A3E /* BackgroundPickerView.swift in Sources */, 4BB88B5B25B7BA50006F6B06 /* Instruments.swift in Sources */, diff --git a/DuckDuckGo/Application/ExperimentalFeatures.swift b/DuckDuckGo/Application/ExperimentalFeatures.swift deleted file mode 100644 index 29d95b145c..0000000000 --- a/DuckDuckGo/Application/ExperimentalFeatures.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// ExperimentalFeatures.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 Foundation - -protocol ExperimentalFeaturesPersistor { - var isHTMLNewTabPageEnabled: Bool { get set } -} - -struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor { - @UserDefaultsWrapper(key: .htmlNewTabPage, defaultValue: false) - var isHTMLNewTabPageEnabled: Bool -} - -protocol ExperimentalFeaturesHandler { - func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) -} - -struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { - func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) { - Task { @MainActor in - WindowControllersManager.shared.mainWindowControllers.forEach { mainWindowController in - if mainWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab { - mainWindowController.mainViewController.browserTabViewController.refreshTab() - } - } - } - } -} - -final class ExperimentalFeatures { - - private var persistor: ExperimentalFeaturesPersistor - private var actionHandler: ExperimentalFeaturesHandler - - init( - persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(), - actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() - ) { - self.persistor = persistor - self.actionHandler = actionHandler - } - - var isHTMLNewTabPageEnabled: Bool { - get { - persistor.isHTMLNewTabPageEnabled - } - set { - if newValue != persistor.isHTMLNewTabPageEnabled { - persistor.isHTMLNewTabPageEnabled = newValue - actionHandler.isHTMLNewTabPageEnabledDidChange(newValue) - } - } - } -} diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift new file mode 100644 index 0000000000..e121325fed --- /dev/null +++ b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift @@ -0,0 +1,48 @@ +// +// ExperimentalFeaturesMenu.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 AppKit + +final class ExperimentalFeaturesMenu: NSMenu { + + let experimentalFeatures: ExperimentalFeatures + + private(set) lazy var htmlNewTabPageMenuItem = NSMenuItem(title: "HTML New Tab Page", action: #selector(toggleHTMLNewTabPage(_:))).targetting(self) + + init(experimentalFeatures: ExperimentalFeatures) { + self.experimentalFeatures = experimentalFeatures + super.init(title: "") + + buildItems { + htmlNewTabPageMenuItem + } + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func update() { + super.update() + htmlNewTabPageMenuItem.state = NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled ? .on : .off + } + + @objc func toggleHTMLNewTabPage(_ sender: NSMenuItem) { + experimentalFeatures.isHTMLNewTabPageEnabled.toggle() + } +} diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index f97c1a3a20..b5809137c1 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -447,11 +447,6 @@ final class MainMenu: NSMenu { updateInternalUserItem() updateRemoteConfigurationInfo() updateAutofillDebugScriptMenuItem() - updateExperimentalFeatures() - } - - private func updateExperimentalFeatures() { - experimentalFeaturesMenu?.items[0].state = NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled ? .on : .off } // MARK: - Bookmarks @@ -623,13 +618,9 @@ final class MainMenu: NSMenu { @MainActor private func setupDebugMenu() -> NSMenu { - let experimentalFeaturesMenu = NSMenu() { - NSMenuItem(title: "HTML New Tab Page", action: #selector(AppDelegate.toggleHTMLNTP(_:)), representedObject: 10) - } - self.experimentalFeaturesMenu = experimentalFeaturesMenu let debugMenu = NSMenu(title: "Debug") { NSMenuItem(title: "Experimental features") - .submenu(experimentalFeaturesMenu) + .submenu(ExperimentalFeaturesMenu(experimentalFeatures: NSApp.delegateTyped.experimentalFeatures)) NSMenuItem.separator() NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() From b20088a68c21f70752d2aa7f4d39aff97b1db439 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 13:32:45 +0100 Subject: [PATCH 10/42] Remove AppDelegate.toggleHTMLNTP --- DuckDuckGo/Application/AppDelegate.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index a35c1caa8d..0bd58b05b0 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -44,11 +44,6 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { - let experimentalFeatures = ExperimentalFeatures() - @objc func toggleHTMLNTP(_ sender: NSMenuItem) { - experimentalFeatures.isHTMLNewTabPageEnabled.toggle() - } - #if DEBUG let disableCVDisplayLinkLogs: Void = { // Disable CVDisplayLink logs @@ -95,6 +90,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? + let experimentalFeatures = ExperimentalFeatures() let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! From ddd4ca0c338ff44bf6d324c19a724e73ba75f967 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 13:33:41 +0100 Subject: [PATCH 11/42] Revert changes to ReleaseNotesUserScript --- DuckDuckGo/Updates/ReleaseNotesUserScript.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 16e7dc7e02..620c2ff3b9 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -26,7 +26,7 @@ import Combine final class ReleaseNotesUserScript: NSObject, Subfeature { lazy var updateController: UpdateControllerProtocol = Application.appDelegate.updateController - var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes"), .exact(hostname: "newtab")]) + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes")]) let featureName: String = "release-notes" weak var broker: UserScriptMessageBroker? weak var webView: WKWebView? { From e2b7dfe75e28288a7e36f134ae54e5fcaccfe052 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 14:40:29 +0100 Subject: [PATCH 12/42] Add ExperimentalFeatures.swift --- .../ExperimentalFeatures.swift | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift new file mode 100644 index 0000000000..29d95b145c --- /dev/null +++ b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift @@ -0,0 +1,70 @@ +// +// ExperimentalFeatures.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 Foundation + +protocol ExperimentalFeaturesPersistor { + var isHTMLNewTabPageEnabled: Bool { get set } +} + +struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor { + @UserDefaultsWrapper(key: .htmlNewTabPage, defaultValue: false) + var isHTMLNewTabPageEnabled: Bool +} + +protocol ExperimentalFeaturesHandler { + func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) +} + +struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { + func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) { + Task { @MainActor in + WindowControllersManager.shared.mainWindowControllers.forEach { mainWindowController in + if mainWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab { + mainWindowController.mainViewController.browserTabViewController.refreshTab() + } + } + } + } +} + +final class ExperimentalFeatures { + + private var persistor: ExperimentalFeaturesPersistor + private var actionHandler: ExperimentalFeaturesHandler + + init( + persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(), + actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() + ) { + self.persistor = persistor + self.actionHandler = actionHandler + } + + var isHTMLNewTabPageEnabled: Bool { + get { + persistor.isHTMLNewTabPageEnabled + } + set { + if newValue != persistor.isHTMLNewTabPageEnabled { + persistor.isHTMLNewTabPageEnabled = newValue + actionHandler.isHTMLNewTabPageEnabledDidChange(newValue) + } + } + } +} From 48f308d2b0a77595bccc270e61d718aa71a4c715 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 7 Nov 2024 15:54:51 +0100 Subject: [PATCH 13/42] WIP internal user decider --- DuckDuckGo/Application/AppDelegate.swift | 3 ++- DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 0bd58b05b0..a0b884dd38 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -90,7 +90,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - let experimentalFeatures = ExperimentalFeatures() + let experimentalFeatures: ExperimentalFeatures let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! @@ -302,6 +302,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) + experimentalFeatures = ExperimentalFeatures(internalUserDecider: internalUserDecider) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift index 29d95b145c..52b82da936 100644 --- a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift +++ b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import BrowserServicesKit import Foundation protocol ExperimentalFeaturesPersistor { @@ -45,13 +46,16 @@ struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { final class ExperimentalFeatures { + let internalUserDecider: InternalUserDecider private var persistor: ExperimentalFeaturesPersistor private var actionHandler: ExperimentalFeaturesHandler init( + internalUserDecider: InternalUserDecider, persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(), actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() ) { + self.internalUserDecider = internalUserDecider self.persistor = persistor self.actionHandler = actionHandler } From 89fc44e01f84cacd3578911fa6ab8198cac8722e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 8 Nov 2024 15:12:50 +0100 Subject: [PATCH 14/42] Integrate ExperimentalFeatures with FeatureFlagger --- DuckDuckGo.xcodeproj/project.pbxproj | 2 + .../xcshareddata/swiftpm/Package.resolved | 9 --- DuckDuckGo/Application/AppDelegate.swift | 2 +- .../ExperimentalFeatures.swift | 76 +++++++++++++++---- .../ExperimentalFeaturesMenu.swift | 14 +++- .../FeatureFlagging/Model/FeatureFlag.swift | 28 ++++++- .../Tab/View/BrowserTabViewController.swift | 4 +- 7 files changed, 105 insertions(+), 30 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9dc2b105eb..ddf80e8f77 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3689,6 +3689,7 @@ 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDuckPlayerView.swift; sourceTree = ""; }; 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerPreferences.swift; sourceTree = ""; }; 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayer.swift; sourceTree = ""; }; + 37F8ABCC2CDE100300CB0294 /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = SOURCE_ROOT; }; 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; 4B02197F25E05FAC00ED7DEA /* FireproofingURLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FireproofingURLExtensions.swift; sourceTree = ""; }; @@ -7684,6 +7685,7 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( + 37F8ABCC2CDE100300CB0294 /* BrowserServicesKit */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cb75d168ef..26c57400e4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "3.0.0" } }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/BrowserServicesKit", - "state" : { - "revision" : "6319be3a8a52024c62cec4320e94536b51f427ee", - "version" : "207.0.0" - } - }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index a0b884dd38..d57252ee3e 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -302,7 +302,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) - experimentalFeatures = ExperimentalFeatures(internalUserDecider: internalUserDecider) + experimentalFeatures = ExperimentalFeatures(internalUserDecider: internalUserDecider, featureFlagger: featureFlagger) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift index 52b82da936..75b6e08cf6 100644 --- a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift +++ b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift @@ -18,22 +18,46 @@ import BrowserServicesKit import Foundation +import Persistence protocol ExperimentalFeaturesPersistor { - var isHTMLNewTabPageEnabled: Bool { get set } + func value(for flag: FeatureFlag) -> Bool? + func set(_ value: Bool?, for flag: FeatureFlag) } struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor { - @UserDefaultsWrapper(key: .htmlNewTabPage, defaultValue: false) - var isHTMLNewTabPageEnabled: Bool + let keyValueStore: KeyValueStoring + + func value(for flag: FeatureFlag) -> Bool? { + let key = key(for: flag) + return keyValueStore.object(forKey: key) as? Bool + } + + func set(_ value: Bool?, for flag: FeatureFlag) { + let key = key(for: flag) + keyValueStore.set(value, forKey: key) + } + + private func key(for flag: FeatureFlag) -> String { + return "local-override.\(flag.rawValue)" + } } protocol ExperimentalFeaturesHandler { - func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) + func flagDidChange(_ featureFlag: FeatureFlag, isEnabled: Bool) } struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { - func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) { + func flagDidChange(_ featureFlag: FeatureFlag, isEnabled: Bool) { + switch featureFlag { + case .htmlNewTabPage: + isHTMLNewTabPageEnabledDidChange(isEnabled) + default: + break + } + } + + private func isHTMLNewTabPageEnabledDidChange(_ isEnabled: Bool) { Task { @MainActor in WindowControllersManager.shared.mainWindowControllers.forEach { mainWindowController in if mainWindowController.mainViewController.tabCollectionViewModel.selectedTabViewModel?.tab.content == .newtab { @@ -47,27 +71,53 @@ struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { final class ExperimentalFeatures { let internalUserDecider: InternalUserDecider + let featureFlagger: FeatureFlagger private var persistor: ExperimentalFeaturesPersistor private var actionHandler: ExperimentalFeaturesHandler init( internalUserDecider: InternalUserDecider, - persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(), + featureFlagger: FeatureFlagger, + persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(keyValueStore: UserDefaults.appConfiguration), actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() ) { self.internalUserDecider = internalUserDecider + self.featureFlagger = featureFlagger self.persistor = persistor self.actionHandler = actionHandler } - var isHTMLNewTabPageEnabled: Bool { - get { - persistor.isHTMLNewTabPageEnabled + func toggleOverride(for featureFlag: FeatureFlag) { + guard internalUserDecider.isInternalUser else { + return + } + let currentValue = persistor.value(for: featureFlag) ?? false + let newValue = !currentValue + persistor.set(!currentValue, for: featureFlag) + actionHandler.flagDidChange(featureFlag, isEnabled: newValue) + } + + func override(for featureFlag: FeatureFlag) -> Bool? { + guard internalUserDecider.isInternalUser else { + return nil } - set { - if newValue != persistor.isHTMLNewTabPageEnabled { - persistor.isHTMLNewTabPageEnabled = newValue - actionHandler.isHTMLNewTabPageEnabledDidChange(newValue) + switch featureFlag { + case .htmlNewTabPage: + return persistor.value(for: featureFlag) + default: + return nil + } + } + + func clearAllOverrides() { + FeatureFlag.allCases.forEach { flag in + guard let override = override(for: flag) else { + return + } + persistor.set(nil, for: flag) + let defaultValue = featureFlagger.isFeatureOn(flag) + if defaultValue != override { + actionHandler.flagDidChange(flag, isEnabled: defaultValue) } } } diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift index e121325fed..8973230675 100644 --- a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift +++ b/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift @@ -30,6 +30,8 @@ final class ExperimentalFeaturesMenu: NSMenu { buildItems { htmlNewTabPageMenuItem + NSMenuItem.separator() + NSMenuItem(title: "Reset All Overrides", action: #selector(resetAllOverrides(_:))).targetting(self) } } @@ -39,10 +41,18 @@ final class ExperimentalFeaturesMenu: NSMenu { override func update() { super.update() - htmlNewTabPageMenuItem.state = NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled ? .on : .off + let featureFlagger = experimentalFeatures.featureFlagger + + let isHTMLNTPOn = featureFlagger.isFeatureOn(.htmlNewTabPage, allowOverride: false) + htmlNewTabPageMenuItem.title = "HTML New Tab Page (default: \(isHTMLNTPOn ? "on" : "off"))" + htmlNewTabPageMenuItem.state = featureFlagger.isFeatureOn(.htmlNewTabPage) ? .on : .off } @objc func toggleHTMLNewTabPage(_ sender: NSMenuItem) { - experimentalFeatures.isHTMLNewTabPageEnabled.toggle() + experimentalFeatures.toggleOverride(for: .htmlNewTabPage) + } + + @objc func resetAllOverrides(_ sender: NSMenuItem) { + experimentalFeatures.clearAllOverrides() } } diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 6f6bc2c95e..552560477b 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -19,7 +19,7 @@ import Foundation import BrowserServicesKit -public enum FeatureFlag: String { +public enum FeatureFlag: String, CaseIterable { case debugMenu case sslCertificatesBypass case phishingDetectionErrorPage @@ -41,6 +41,8 @@ public enum FeatureFlag: String { /// https://app.asana.com/0/72649045549333/1208231259093710/f case networkProtectionUserTips + + case htmlNewTabPage } extension FeatureFlag: FeatureFlagSourceProviding { @@ -66,12 +68,32 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(AutofillSubfeature.credentialsImportPromotionForExistingUsers)) case .networkProtectionUserTips: return .remoteDevelopment(.subfeature(NetworkProtectionSubfeature.userTips)) + case .htmlNewTabPage: + return .disabled } } } +protocol LocalFeatureFlagOverriding { + func localOverride(for featureFlag: FeatureFlag) -> Bool? +} + +extension FeatureFlag: LocalFeatureFlagOverriding { + func localOverride(for featureFlag: FeatureFlag) -> Bool? { + NSApp.delegateTyped.experimentalFeatures.override(for: featureFlag) + } + + var localOverride: Bool? { + localOverride(for: self) + } +} + extension FeatureFlagger { - public func isFeatureOn(_ featureFlag: FeatureFlag) -> Bool { - isFeatureOn(forProvider: featureFlag) + + public func isFeatureOn(_ featureFlag: FeatureFlag, allowOverride: Bool = true) -> Bool { + if allowOverride, let localOverride = featureFlag.localOverride { + return localOverride + } + return isFeatureOn(forProvider: featureFlag) } } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 2d8feb22dc..b187a2b989 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -777,7 +777,7 @@ final class BrowserTabViewController: NSViewController { updateTabIfNeeded(tabViewModel: tabViewModel) case .newtab: - if NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled { + if NSApp.delegateTyped.featureFlagger.isFeatureOn(.htmlNewTabPage) { updateTabIfNeeded(tabViewModel: tabViewModel) } else { removeAllTabContent() @@ -845,7 +845,7 @@ final class BrowserTabViewController: NSViewController { case .onboarding: return case .newtab: - containsHostingView = !NSApp.delegateTyped.experimentalFeatures.isHTMLNewTabPageEnabled + containsHostingView = !NSApp.delegateTyped.featureFlagger.isFeatureOn(.htmlNewTabPage) case .settings: containsHostingView = true default: From c195b033a0fe72d14f7c13b598716870381cd33e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 8 Nov 2024 15:25:07 +0100 Subject: [PATCH 15/42] ExperimentalFeatures -> FeatureFlagOverrides --- DuckDuckGo.xcodeproj/project.pbxproj | 32 +++++++------------ DuckDuckGo/Application/AppDelegate.swift | 4 +-- .../FeatureFlagging/Model/FeatureFlag.swift | 2 +- .../Model/FeatureFlagOverrides.swift} | 18 +++++------ .../Model/FeatureFlagOverridesMenu.swift} | 14 ++++---- DuckDuckGo/Menus/MainMenu.swift | 4 +-- 6 files changed, 33 insertions(+), 41 deletions(-) rename DuckDuckGo/{ExperimentalFeatures/ExperimentalFeatures.swift => FeatureFlagging/Model/FeatureFlagOverrides.swift} (85%) rename DuckDuckGo/{ExperimentalFeatures/ExperimentalFeaturesMenu.swift => FeatureFlagging/Model/FeatureFlagOverridesMenu.swift} (81%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ddf80e8f77..aa2cbbda4f 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1058,10 +1058,10 @@ 370C230F2C76A3D600A80A3E /* SettingsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23042C76A31F00A80A3E /* SettingsGrid.swift */; }; 370C23112C7747E200A80A3E /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23102C7747E200A80A3E /* ImageProcessor.swift */; }; 370C23122C7747E200A80A3E /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370C23102C7747E200A80A3E /* ImageProcessor.swift */; }; - 37115A0F2CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */; }; - 37115A102CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */; }; - 37115A142CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */; }; - 37115A152CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */; }; + 37115A0F2CDCE63800F2FF5C /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A0E2CDCE63300F2FF5C /* FeatureFlagOverridesMenu.swift */; }; + 37115A102CDCE63800F2FF5C /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A0E2CDCE63300F2FF5C /* FeatureFlagOverridesMenu.swift */; }; + 37115A142CDCE65D00F2FF5C /* FeatureFlagOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A122CDCE65D00F2FF5C /* FeatureFlagOverrides.swift */; }; + 37115A152CDCE65D00F2FF5C /* FeatureFlagOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37115A122CDCE65D00F2FF5C /* FeatureFlagOverrides.swift */; }; 371209212C232E3F003ADF3D /* RemoteMessagingClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */; }; 371209232C232E66003ADF3D /* RemoteMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 371209222C232E66003ADF3D /* RemoteMessaging */; }; 371209252C232E6C003ADF3D /* RemoteMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 371209242C232E6C003ADF3D /* RemoteMessaging */; }; @@ -3551,8 +3551,8 @@ 370C23082C76A39900A80A3E /* BackgroundCategoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundCategoryView.swift; sourceTree = ""; }; 370C230A2C76A3BC00A80A3E /* BackgroundThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThumbnailView.swift; sourceTree = ""; }; 370C23102C7747E200A80A3E /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = ""; }; - 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeaturesMenu.swift; sourceTree = ""; }; - 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExperimentalFeatures.swift; sourceTree = ""; }; + 37115A0E2CDCE63300F2FF5C /* FeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverridesMenu.swift; sourceTree = ""; }; + 37115A122CDCE65D00F2FF5C /* FeatureFlagOverrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverrides.swift; sourceTree = ""; }; 371209202C232E3F003ADF3D /* RemoteMessagingClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingClient.swift; sourceTree = ""; }; 371209292C2333A0003ADF3D /* RemoteMessagingDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingDatabase.swift; sourceTree = ""; }; 3712092B2C23383C003ADF3D /* RemoteMessagingStoreErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingStoreErrorHandling.swift; sourceTree = ""; }; @@ -5276,6 +5276,8 @@ isa = PBXGroup; children = ( EECE10E429DD77E60044D027 /* FeatureFlag.swift */, + 37115A122CDCE65D00F2FF5C /* FeatureFlagOverrides.swift */, + 37115A0E2CDCE63300F2FF5C /* FeatureFlagOverridesMenu.swift */, ); path = Model; sourceTree = ""; @@ -5630,15 +5632,6 @@ path = HomePageSettings; sourceTree = ""; }; - 37115A132CDCE65D00F2FF5C /* ExperimentalFeatures */ = { - isa = PBXGroup; - children = ( - 37115A122CDCE65D00F2FF5C /* ExperimentalFeatures.swift */, - 37115A0E2CDCE63300F2FF5C /* ExperimentalFeaturesMenu.swift */, - ); - path = ExperimentalFeatures; - sourceTree = ""; - }; 3712091F2C232E2B003ADF3D /* RemoteMessaging */ = { isa = PBXGroup; children = ( @@ -7763,7 +7756,6 @@ 4B5F15032A1570F10060320F /* DuckDuckGoDebug.entitlements */, 4B65143C26392483005B46EB /* Email */, B68412192B6A16030092F66A /* ErrorPage */, - 37115A132CDCE65D00F2FF5C /* ExperimentalFeatures */, AA5FA695275F823900DCE9C9 /* Favicons */, 1D36E651298A84F600AA485D /* FeatureFlagging */, AA3863C227A1E1C000749AB5 /* Feedback */, @@ -11246,7 +11238,7 @@ 3706FB13293F65D500E42796 /* Bookmark.swift in Sources */, 3706FB14293F65D500E42796 /* ConnectBitwardenViewModel.swift in Sources */, 3706FB15293F65D500E42796 /* NSNotificationName+DataImport.swift in Sources */, - 37115A152CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */, + 37115A152CDCE65D00F2FF5C /* FeatureFlagOverrides.swift in Sources */, EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */, 3706FB16293F65D500E42796 /* StoredPermission.swift in Sources */, 3706FB17293F65D500E42796 /* FirePopoverCollectionViewHeader.swift in Sources */, @@ -11416,7 +11408,7 @@ 567A23D82C8871290010F66C /* OnboardingFireButtonDialogViewModel.swift in Sources */, 37FD78122A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */, 1D4B03D62CA4432000224E99 /* BookmarkUrlExtension.swift in Sources */, - 37115A102CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */, + 37115A102CDCE63800F2FF5C /* FeatureFlagOverridesMenu.swift in Sources */, 987799F42999993C005D8EB6 /* LegacyBookmarksStoreMigration.swift in Sources */, 3706FB7A293F65D500E42796 /* FileDownloadManager.swift in Sources */, 3706FB7B293F65D500E42796 /* BookmarkImport.swift in Sources */, @@ -12890,7 +12882,7 @@ 1D6A492029CF7A490011DF74 /* NSPopoverExtension.swift in Sources */, 37CC53F427E8D4620028713D /* NSPathControlView.swift in Sources */, B6BF5D8929470BC4006742B1 /* HTTPSUpgradeTabExtension.swift in Sources */, - 37115A142CDCE65D00F2FF5C /* ExperimentalFeatures.swift in Sources */, + 37115A142CDCE65D00F2FF5C /* FeatureFlagOverrides.swift in Sources */, 1D36E65B298ACD2900AA485D /* AppIconChanger.swift in Sources */, 4B4D60E22A0C883A00BCD287 /* AppMain.swift in Sources */, 7BCB90C22C18626E008E3543 /* VPNControllerXPCClient+ConvenienceInitializers.swift in Sources */, @@ -12916,7 +12908,7 @@ AAE246F8270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift in Sources */, 314872742CC653D500EEF89B /* AIChatOnboardingPopover.swift in Sources */, AAB7320926DD0CD9002FACF9 /* FireViewController.swift in Sources */, - 37115A0F2CDCE63800F2FF5C /* ExperimentalFeaturesMenu.swift in Sources */, + 37115A0F2CDCE63800F2FF5C /* FeatureFlagOverridesMenu.swift in Sources */, 31C26A0D2CBE9DFE00FFF462 /* AIChatPreferences.swift in Sources */, 4B92928C26670D1700AD2C21 /* OutlineSeparatorViewCell.swift in Sources */, 8426108D2C9811F30070D5F9 /* KeyEquivalentView.swift in Sources */, diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index d57252ee3e..a1abbd2220 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -90,7 +90,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - let experimentalFeatures: ExperimentalFeatures + let featureFlagOverrides: FeatureFlagOverrides let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! @@ -302,7 +302,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) - experimentalFeatures = ExperimentalFeatures(internalUserDecider: internalUserDecider, featureFlagger: featureFlagger) + featureFlagOverrides = FeatureFlagOverrides(internalUserDecider: internalUserDecider, featureFlagger: featureFlagger) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift index 552560477b..23dc1c0689 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlag.swift @@ -80,7 +80,7 @@ protocol LocalFeatureFlagOverriding { extension FeatureFlag: LocalFeatureFlagOverriding { func localOverride(for featureFlag: FeatureFlag) -> Bool? { - NSApp.delegateTyped.experimentalFeatures.override(for: featureFlag) + NSApp.delegateTyped.featureFlagOverrides.override(for: featureFlag) } var localOverride: Bool? { diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift similarity index 85% rename from DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift rename to DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift index 75b6e08cf6..1bf683aeb8 100644 --- a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeatures.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift @@ -20,12 +20,12 @@ import BrowserServicesKit import Foundation import Persistence -protocol ExperimentalFeaturesPersistor { +protocol FeatureFlagOverridesPersistor { func value(for flag: FeatureFlag) -> Bool? func set(_ value: Bool?, for flag: FeatureFlag) } -struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor { +struct FeatureFlagOverridesUserDefaultsPersistor: FeatureFlagOverridesPersistor { let keyValueStore: KeyValueStoring func value(for flag: FeatureFlag) -> Bool? { @@ -43,11 +43,11 @@ struct ExperimentalFeaturesUserDefaultsPersistor: ExperimentalFeaturesPersistor } } -protocol ExperimentalFeaturesHandler { +protocol FeatureFlagOverridesHandler { func flagDidChange(_ featureFlag: FeatureFlag, isEnabled: Bool) } -struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { +struct FeatureFlagOverridesDefaultHandler: FeatureFlagOverridesHandler { func flagDidChange(_ featureFlag: FeatureFlag, isEnabled: Bool) { switch featureFlag { case .htmlNewTabPage: @@ -68,18 +68,18 @@ struct ExperimentalFeaturesDefaultHandler: ExperimentalFeaturesHandler { } } -final class ExperimentalFeatures { +final class FeatureFlagOverrides { let internalUserDecider: InternalUserDecider let featureFlagger: FeatureFlagger - private var persistor: ExperimentalFeaturesPersistor - private var actionHandler: ExperimentalFeaturesHandler + private var persistor: FeatureFlagOverridesPersistor + private var actionHandler: FeatureFlagOverridesHandler init( internalUserDecider: InternalUserDecider, featureFlagger: FeatureFlagger, - persistor: ExperimentalFeaturesPersistor = ExperimentalFeaturesUserDefaultsPersistor(keyValueStore: UserDefaults.appConfiguration), - actionHandler: ExperimentalFeaturesHandler = ExperimentalFeaturesDefaultHandler() + persistor: FeatureFlagOverridesPersistor = FeatureFlagOverridesUserDefaultsPersistor(keyValueStore: UserDefaults.appConfiguration), + actionHandler: FeatureFlagOverridesHandler = FeatureFlagOverridesDefaultHandler() ) { self.internalUserDecider = internalUserDecider self.featureFlagger = featureFlagger diff --git a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverridesMenu.swift similarity index 81% rename from DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift rename to DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverridesMenu.swift index 8973230675..733e7a5599 100644 --- a/DuckDuckGo/ExperimentalFeatures/ExperimentalFeaturesMenu.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverridesMenu.swift @@ -18,14 +18,14 @@ import AppKit -final class ExperimentalFeaturesMenu: NSMenu { +final class FeatureFlagOverridesMenu: NSMenu { - let experimentalFeatures: ExperimentalFeatures + let featureFlagOverrides: FeatureFlagOverrides private(set) lazy var htmlNewTabPageMenuItem = NSMenuItem(title: "HTML New Tab Page", action: #selector(toggleHTMLNewTabPage(_:))).targetting(self) - init(experimentalFeatures: ExperimentalFeatures) { - self.experimentalFeatures = experimentalFeatures + init(featureFlagOverrides: FeatureFlagOverrides) { + self.featureFlagOverrides = featureFlagOverrides super.init(title: "") buildItems { @@ -41,7 +41,7 @@ final class ExperimentalFeaturesMenu: NSMenu { override func update() { super.update() - let featureFlagger = experimentalFeatures.featureFlagger + let featureFlagger = featureFlagOverrides.featureFlagger let isHTMLNTPOn = featureFlagger.isFeatureOn(.htmlNewTabPage, allowOverride: false) htmlNewTabPageMenuItem.title = "HTML New Tab Page (default: \(isHTMLNTPOn ? "on" : "off"))" @@ -49,10 +49,10 @@ final class ExperimentalFeaturesMenu: NSMenu { } @objc func toggleHTMLNewTabPage(_ sender: NSMenuItem) { - experimentalFeatures.toggleOverride(for: .htmlNewTabPage) + featureFlagOverrides.toggleOverride(for: .htmlNewTabPage) } @objc func resetAllOverrides(_ sender: NSMenuItem) { - experimentalFeatures.clearAllOverrides() + featureFlagOverrides.clearAllOverrides() } } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index b5809137c1..61a3eea81e 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -619,8 +619,8 @@ final class MainMenu: NSMenu { @MainActor private func setupDebugMenu() -> NSMenu { let debugMenu = NSMenu(title: "Debug") { - NSMenuItem(title: "Experimental features") - .submenu(ExperimentalFeaturesMenu(experimentalFeatures: NSApp.delegateTyped.experimentalFeatures)) + NSMenuItem(title: "Feature Flag Overrides") + .submenu(FeatureFlagOverridesMenu(featureFlagOverrides: NSApp.delegateTyped.featureFlagOverrides)) NSMenuItem.separator() NSMenuItem(title: "Open Vanilla Browser", action: #selector(MainViewController.openVanillaBrowser)).withAccessibilityIdentifier("MainMenu.openVanillaBrowser") NSMenuItem.separator() From 7a46dad9af5f00ef8b9d21756bd6a2ef8171017b Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 8 Nov 2024 15:38:28 +0100 Subject: [PATCH 16/42] Only allow for overridding HTML NTP flag --- .../FeatureFlagging/Model/FeatureFlagOverrides.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift index 1bf683aeb8..ae43f5d0aa 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift @@ -91,6 +91,14 @@ final class FeatureFlagOverrides { guard internalUserDecider.isInternalUser else { return } + + switch featureFlag { + case .htmlNewTabPage: + break + default: + return + } + let currentValue = persistor.value(for: featureFlag) ?? false let newValue = !currentValue persistor.set(!currentValue, for: featureFlag) From 037d823d6b84f717529931402b7ee75b4bbf2c9a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 8 Nov 2024 15:52:12 +0100 Subject: [PATCH 17/42] Fix SwiftLint violation --- DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift index ae43f5d0aa..fe1e297574 100644 --- a/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift +++ b/DuckDuckGo/FeatureFlagging/Model/FeatureFlagOverrides.swift @@ -1,5 +1,5 @@ // -// ExperimentalFeatures.swift +// FeatureFlagOverrides.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // From 95cd2bc3d003ffdd417b31828132c3f9a7ada041 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 12 Nov 2024 23:36:26 +0100 Subject: [PATCH 18/42] Move FeatureFlagOverrides to BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 9 ++ DuckDuckGo/Application/AppDelegate.swift | 11 +- .../FeatureFlagOverridesMenu.swift | 53 +++++--- .../Sources/FeatureFlags/FeatureFlag.swift | 21 +++- .../FeatureFlags/FeatureFlagOverrides.swift | 114 ------------------ .../OverridableFeatureFlagger.swift | 51 -------- 7 files changed, 73 insertions(+), 192 deletions(-) delete mode 100644 LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlagOverrides.swift delete mode 100644 LocalPackages/FeatureFlags/Sources/FeatureFlags/OverridableFeatureFlagger.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 07d626a461..9be596e9ac 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3699,7 +3699,6 @@ 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDuckPlayerView.swift; sourceTree = ""; }; 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerPreferences.swift; sourceTree = ""; }; 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayer.swift; sourceTree = ""; }; - 37F8ABCC2CDE100300CB0294 /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = SOURCE_ROOT; }; 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverridesMenu.swift; sourceTree = ""; }; 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; 4B0135CD2729F1AA00D54834 /* NSPasteboardExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSPasteboardExtension.swift; sourceTree = ""; }; @@ -7684,7 +7683,6 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( - 37F8ABCC2CDE100300CB0294 /* BrowserServicesKit */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, @@ -15023,8 +15021,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 209.0.0; + branch = "dominik/feature-flag-overrides"; + kind = branch; }; }; 9FF521422BAA8FF300B9819B /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index f548f86b61..2bb2cd958e 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "3.0.0" } }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "branch" : "dominik/feature-flag-overrides", + "revision" : "1e11729a35087cb83a6dfe5d026294760073c28e" + } + }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 8dd3596481..5ec16cd099 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -78,7 +78,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private var grammarFeaturesManager = GrammarFeaturesManager() let internalUserDecider: InternalUserDecider private var isInternalUserSharingCancellable: AnyCancellable? - let featureFlagger: OverridableFeatureFlagger + let featureFlagger: FeatureFlagger private var appIconChanger: AppIconChanger! private var autoClearHandler: AutoClearHandler! private(set) var autofillPixelReporter: AutofillPixelReporter? @@ -263,11 +263,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate { activeRemoteMessageModel = ActiveRemoteMessageModel(remoteMessagingStore: nil, remoteMessagingAvailabilityProvider: nil) } - featureFlagger = OverridableFeatureFlagger( - defaultFlagger: DefaultFeatureFlagger( - internalUserDecider: internalUserDecider, - privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager - ), overrides: FeatureFlagOverrides( + featureFlagger = DefaultFeatureFlagger( + internalUserDecider: internalUserDecider, + privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, + localOverrides: FeatureFlagOverrides( keyValueStore: UserDefaults.appConfiguration, actionHandler: FeatureFlagOverridesDefaultHandler() ) diff --git a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift index 384ab01597..ea9d7d0af6 100644 --- a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift @@ -1,5 +1,5 @@ // -// ExperimentalFeaturesMenu.swift +// FeatureFlagOverridesMenu.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -17,11 +17,13 @@ // import AppKit +import BrowserServicesKit import FeatureFlags struct FeatureFlagOverridesDefaultHandler: FeatureFlagOverridesHandler { - func flagDidChange(_ featureFlag: FeatureFlag, isEnabled: Bool) { - switch featureFlag { + func flagDidChange(_ featureFlag: Flag, isEnabled: Bool) { + guard let flag = featureFlag as? FeatureFlag else { return } + switch flag { case .htmlNewTabPage: isHTMLNewTabPageEnabledDidChange(isEnabled) default: @@ -40,19 +42,23 @@ struct FeatureFlagOverridesDefaultHandler: FeatureFlagOverridesHandler { } } - final class FeatureFlagOverridesMenu: NSMenu { - let featureFlagger: OverridableFeatureFlagger - - private(set) lazy var htmlNewTabPageMenuItem = NSMenuItem(title: "HTML New Tab Page", action: #selector(toggleHTMLNewTabPage(_:))).targetting(self) + let featureFlagger: FeatureFlagger - init(featureFlagOverrides: OverridableFeatureFlagger) { + init(featureFlagOverrides: FeatureFlagger) { self.featureFlagger = featureFlagOverrides super.init(title: "") buildItems { - htmlNewTabPageMenuItem + FeatureFlag.allCases.filter(\.supportsLocalOverriding).map { flag in + NSMenuItem( + title: "\(flag.rawValue) (default: \(featureFlagger.isFeatureOn(flag, allowOverride: false) ? "on" : "off"))", + action: #selector(toggleFeatureFlag(_:)), + target: self, + representedObject: flag + ) + } NSMenuItem.separator() NSMenuItem(title: "Reset All Overrides", action: #selector(resetAllOverrides(_:))).targetting(self) } @@ -64,18 +70,33 @@ final class FeatureFlagOverridesMenu: NSMenu { override func update() { super.update() - let featureFlagger = featureFlagger - let isHTMLNTPOn = featureFlagger.isFeatureOn(.htmlNewTabPage, allowOverride: false) - htmlNewTabPageMenuItem.title = "HTML New Tab Page (default: \(isHTMLNTPOn ? "on" : "off"))" - htmlNewTabPageMenuItem.state = featureFlagger.isFeatureOn(.htmlNewTabPage) ? .on : .off + items.forEach { item in + guard let flag = item.representedObject as? FeatureFlag else { + return + } + item.title = "\(flag.rawValue) (default: \(defaultValue(for: flag)), override: \(overrideValue(for: flag)))" + item.state = featureFlagger.localOverrides?.override(for: flag) == true ? .on : .off + } + } + + private func defaultValue(for flag: FeatureFlag) -> String { + featureFlagger.isFeatureOn(flag, allowOverride: false) ? "on" : "off" + } + + private func overrideValue(for flag: FeatureFlag) -> String { + guard let override = featureFlagger.localOverrides?.override(for: flag) else { + return "none" + } + return override ? "on" : "off" } - @objc func toggleHTMLNewTabPage(_ sender: NSMenuItem) { - featureFlagger.overrides.toggleOverride(for: .htmlNewTabPage) + @objc func toggleFeatureFlag(_ sender: NSMenuItem) { + guard let featureFlag = sender.representedObject as? FeatureFlag else { return } + featureFlagger.localOverrides?.toggleOverride(for: featureFlag) } @objc func resetAllOverrides(_ sender: NSMenuItem) { - featureFlagger.overrides.clearAllOverrides() + featureFlagger.localOverrides?.clearAllOverrides(for: FeatureFlag.self) } } diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift index 435e127029..203012a946 100644 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift +++ b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift @@ -48,7 +48,16 @@ public enum FeatureFlag: String, CaseIterable { case htmlNewTabPage } -extension FeatureFlag: FeatureFlagSourceProviding { +extension FeatureFlag: FeatureFlagProtocol { + public var supportsLocalOverriding: Bool { + switch self { + case .htmlNewTabPage: + return true + default: + return false + } + } + public var source: FeatureFlagSource { switch self { case .debugMenu: @@ -78,3 +87,13 @@ extension FeatureFlag: FeatureFlagSourceProviding { } } } + +public extension FeatureFlagger { + + func isFeatureOn(_ featureFlag: FeatureFlag, allowOverride: Bool = true) -> Bool { + if internalUserDecider.isInternalUser, allowOverride, let localOverride = localOverrides?.override(for: featureFlag) { + return localOverride + } + return isFeatureOn(forProvider: featureFlag) + } +} diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlagOverrides.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlagOverrides.swift deleted file mode 100644 index d9bdb5998e..0000000000 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlagOverrides.swift +++ /dev/null @@ -1,114 +0,0 @@ -// -// FeatureFlagOverrides.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 BrowserServicesKit -import Foundation -import Persistence - -public protocol FeatureFlagOverridesPersistor { - func value(for flag: FeatureFlag) -> Bool? - func set(_ value: Bool?, for flag: FeatureFlag) -} - -public struct FeatureFlagOverridesUserDefaultsPersistor: FeatureFlagOverridesPersistor { - public let keyValueStore: KeyValueStoring - - public func value(for flag: FeatureFlag) -> Bool? { - let key = key(for: flag) - return keyValueStore.object(forKey: key) as? Bool - } - - public func set(_ value: Bool?, for flag: FeatureFlag) { - let key = key(for: flag) - keyValueStore.set(value, forKey: key) - } - - private func key(for flag: FeatureFlag) -> String { - return "localOverride\(flag.rawValue.capitalizedFirstLetter)" - } -} - -private extension String { - var capitalizedFirstLetter: String { - return prefix(1).capitalized + dropFirst() - } -} - -public protocol FeatureFlagOverridesHandler { - func flagDidChange(_ featureFlag: FeatureFlag, isEnabled: Bool) -} - -public final class FeatureFlagOverrides { - - private var persistor: FeatureFlagOverridesPersistor - private var actionHandler: FeatureFlagOverridesHandler - - public convenience init( - keyValueStore: KeyValueStoring, - actionHandler: FeatureFlagOverridesHandler - ) { - self.init( - persistor: FeatureFlagOverridesUserDefaultsPersistor(keyValueStore: keyValueStore), - actionHandler: actionHandler - ) - } - - public init( - persistor: FeatureFlagOverridesPersistor, - actionHandler: FeatureFlagOverridesHandler - ) { - self.persistor = persistor - self.actionHandler = actionHandler - } - - public func toggleOverride(for featureFlag: FeatureFlag) { - switch featureFlag { - case .htmlNewTabPage: - break - default: - return - } - - let currentValue = persistor.value(for: featureFlag) ?? false - let newValue = !currentValue - persistor.set(newValue, for: featureFlag) - actionHandler.flagDidChange(featureFlag, isEnabled: newValue) - } - - public func override(for featureFlag: FeatureFlag) -> Bool? { - switch featureFlag { - case .htmlNewTabPage: - return persistor.value(for: featureFlag) - default: - return nil - } - } - - public func clearAllOverrides() { -// FeatureFlag.allCases.forEach { flag in -// guard let override = override(for: flag) else { -// return -// } -// persistor.set(nil, for: flag) -// let defaultValue = featureFlagger.isFeatureOn(flag) -// if defaultValue != override { -// actionHandler.flagDidChange(flag, isEnabled: defaultValue) -// } -// } - } -} diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/OverridableFeatureFlagger.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/OverridableFeatureFlagger.swift deleted file mode 100644 index 7b7e5b40c4..0000000000 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/OverridableFeatureFlagger.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// OverridableFeatureFlagger.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 BrowserServicesKit - -public final class OverridableFeatureFlagger: FeatureFlagger { - - public let defaultFlagger: DefaultFeatureFlagger - public let overrides: FeatureFlagOverrides - - public init(defaultFlagger: DefaultFeatureFlagger, overrides: FeatureFlagOverrides) { - self.defaultFlagger = defaultFlagger - self.overrides = overrides - } - - public func isFeatureOn(forProvider provider: F) -> Bool { - defaultFlagger.isFeatureOn(forProvider: provider) - } - - public func isFeatureOn(_ featureFlag: FeatureFlag, allowOverride: Bool = true) -> Bool { - if defaultFlagger.internalUserDecider.isInternalUser, allowOverride, let localOverride = overrides.override(for: featureFlag) { - return localOverride - } - return isFeatureOn(forProvider: featureFlag) - } -} - -public extension FeatureFlagger { - - func isFeatureOn(_ featureFlag: FeatureFlag) -> Bool { - if let overridableFlagger = self as? OverridableFeatureFlagger { - return overridableFlagger.isFeatureOn(featureFlag, allowOverride: true) - } - return isFeatureOn(forProvider: featureFlag) - } -} From 03221fecf8efb0146429b77d1d70182db00d26c3 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 12 Nov 2024 23:44:03 +0100 Subject: [PATCH 19/42] Update overrides menu --- .../xcshareddata/swiftpm/Package.resolved | 2 +- .../FeatureFlagOverridesMenu.swift | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2bb2cd958e..4ff579bec9 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "dominik/feature-flag-overrides", - "revision" : "1e11729a35087cb83a6dfe5d026294760073c28e" + "revision" : "cfbf585bdf15436697af25420aeb39ebd07242b6" } }, { diff --git a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift index ea9d7d0af6..d7fe8d0d7f 100644 --- a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift @@ -58,9 +58,17 @@ final class FeatureFlagOverridesMenu: NSMenu { target: self, representedObject: flag ) + .submenu(NSMenu(items: [ + NSMenuItem( + title: "Remove Override", + action: #selector(resetOverride(_:)), + target: self, + representedObject: flag + ) + ])) } NSMenuItem.separator() - NSMenuItem(title: "Reset All Overrides", action: #selector(resetAllOverrides(_:))).targetting(self) + NSMenuItem(title: "Remove All Overrides", action: #selector(resetAllOverrides(_:))).targetting(self) } } @@ -96,6 +104,11 @@ final class FeatureFlagOverridesMenu: NSMenu { featureFlagger.localOverrides?.toggleOverride(for: featureFlag) } + @objc func resetOverride(_ sender: NSMenuItem) { + guard let featureFlag = sender.representedObject as? FeatureFlag else { return } + featureFlagger.localOverrides?.clearOverride(for: featureFlag) + } + @objc func resetAllOverrides(_ sender: NSMenuItem) { featureFlagger.localOverrides?.clearAllOverrides(for: FeatureFlag.self) } From fe3999a2b93ae2863927bcf71aff3b547c56b8f2 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 15:12:32 +0100 Subject: [PATCH 20/42] Update BSK --- .../xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/Application/AppDelegate.swift | 5 +++-- .../InternalUserDecider/FeatureFlagOverridesMenu.swift | 2 +- .../FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift | 7 ++----- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4ff579bec9..dcfc68db8f 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "dominik/feature-flag-overrides", - "revision" : "cfbf585bdf15436697af25420aeb39ebd07242b6" + "revision" : "a5d5759b584361fa072814f1f039d3620bd41596" } }, { diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 5ec16cd099..afd95a3334 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -266,10 +266,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate { featureFlagger = DefaultFeatureFlagger( internalUserDecider: internalUserDecider, privacyConfigManager: AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, - localOverrides: FeatureFlagOverrides( + localOverrides: FeatureFlagLocalOverrides( keyValueStore: UserDefaults.appConfiguration, actionHandler: FeatureFlagOverridesDefaultHandler() - ) + ), + for: FeatureFlag.self ) onboardingStateMachine = ContextualOnboardingStateMachine() diff --git a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift index d7fe8d0d7f..6d62ecead1 100644 --- a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift @@ -20,7 +20,7 @@ import AppKit import BrowserServicesKit import FeatureFlags -struct FeatureFlagOverridesDefaultHandler: FeatureFlagOverridesHandler { +struct FeatureFlagOverridesDefaultHandler: FeatureFlagLocalOverridesHandler { func flagDidChange(_ featureFlag: Flag, isEnabled: Bool) { guard let flag = featureFlag as? FeatureFlag else { return } switch flag { diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift index 203012a946..46cb0f8103 100644 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift +++ b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift @@ -90,10 +90,7 @@ extension FeatureFlag: FeatureFlagProtocol { public extension FeatureFlagger { - func isFeatureOn(_ featureFlag: FeatureFlag, allowOverride: Bool = true) -> Bool { - if internalUserDecider.isInternalUser, allowOverride, let localOverride = localOverrides?.override(for: featureFlag) { - return localOverride - } - return isFeatureOn(forProvider: featureFlag) + func isFeatureOn(_ featureFlag: FeatureFlag) -> Bool { + isFeatureOn(featureFlag) } } From 05e9407e5c3c82ed1b5328dc1e93e5389d328433 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 15:41:12 +0100 Subject: [PATCH 21/42] Update overrides menu --- .../FeatureFlagOverridesMenu.swift | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift index 6d62ecead1..09171daaff 100644 --- a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift @@ -46,27 +46,29 @@ final class FeatureFlagOverridesMenu: NSMenu { let featureFlagger: FeatureFlagger + let setInternalUserStateItem: NSMenuItem = { + let item = NSMenuItem(title: "Set Internal User State First") + item.isEnabled = false + return item + }() + init(featureFlagOverrides: FeatureFlagger) { self.featureFlagger = featureFlagOverrides super.init(title: "") buildItems { + setInternalUserStateItem + NSMenuItem.separator() + FeatureFlag.allCases.filter(\.supportsLocalOverriding).map { flag in NSMenuItem( - title: "\(flag.rawValue) (default: \(featureFlagger.isFeatureOn(flag, allowOverride: false) ? "on" : "off"))", + title: "\(flag.rawValue) (default: \(featureFlagger.isFeatureOn(for: flag, allowOverride: false) ? "on" : "off"))", action: #selector(toggleFeatureFlag(_:)), target: self, representedObject: flag ) - .submenu(NSMenu(items: [ - NSMenuItem( - title: "Remove Override", - action: #selector(resetOverride(_:)), - target: self, - representedObject: flag - ) - ])) } + NSMenuItem.separator() NSMenuItem(title: "Remove All Overrides", action: #selector(resetAllOverrides(_:))).targetting(self) } @@ -83,13 +85,30 @@ final class FeatureFlagOverridesMenu: NSMenu { guard let flag = item.representedObject as? FeatureFlag else { return } + item.isHidden = !featureFlagger.internalUserDecider.isInternalUser item.title = "\(flag.rawValue) (default: \(defaultValue(for: flag)), override: \(overrideValue(for: flag)))" - item.state = featureFlagger.localOverrides?.override(for: flag) == true ? .on : .off + let override = featureFlagger.localOverrides?.override(for: flag) + item.state = override == true ? .on : .off + + if override != nil { + item.submenu = NSMenu(items: [ + NSMenuItem( + title: "Remove Override", + action: #selector(resetOverride(_:)), + target: self, + representedObject: flag + ) + ]) + } else { + item.submenu = nil + } } + + setInternalUserStateItem.isHidden = featureFlagger.internalUserDecider.isInternalUser } private func defaultValue(for flag: FeatureFlag) -> String { - featureFlagger.isFeatureOn(flag, allowOverride: false) ? "on" : "off" + featureFlagger.isFeatureOn(for: flag, allowOverride: false) ? "on" : "off" } private func overrideValue(for flag: FeatureFlag) -> String { From 0b7c142d91d553f56f8679e38237be3527ae9e04 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 15:52:12 +0100 Subject: [PATCH 22/42] Update mocks --- .../PhishingDetectionIntegrationTests.swift | 5 ++++- UnitTests/Menus/MainMenuTests.swift | 5 ++++- UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift b/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift index d10d7e0c6b..7b1bcfd89e 100644 --- a/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift +++ b/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift @@ -148,7 +148,10 @@ class PhishingDetectionIntegrationTests: XCTestCase { } class MockFeatureFlagger: FeatureFlagger { - func isFeatureOn(forProvider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding { + var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: MockInternalUserStoring()) + var localOverrides: FeatureFlagLocalOverriding? + + func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { return true } } diff --git a/UnitTests/Menus/MainMenuTests.swift b/UnitTests/Menus/MainMenuTests.swift index 56042fbf87..59cde2b7e2 100644 --- a/UnitTests/Menus/MainMenuTests.swift +++ b/UnitTests/Menus/MainMenuTests.swift @@ -194,7 +194,10 @@ class MainMenuTests: XCTestCase { } private class DummyFeatureFlagger: FeatureFlagger { - func isFeatureOn(forProvider: F) -> Bool { + var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: MockInternalUserStoring()) + var localOverrides: FeatureFlagLocalOverriding? + + func isFeatureOn(for: Flag, allowOverride: Bool) -> Bool { false } } diff --git a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift index ffa3b7d9c6..14e82673f2 100644 --- a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift +++ b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift @@ -480,8 +480,11 @@ class ChallangeSender: URLAuthenticationChallengeSender { } class MockFeatureFlagger: FeatureFlagger { + var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: MockInternalUserStoring()) + var localOverrides: FeatureFlagLocalOverriding? + var isFeatureOn = true - func isFeatureOn(forProvider: F) -> Bool where F: BrowserServicesKit.FeatureFlagSourceProviding { + func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { return isFeatureOn } } From 70f4916fcb437adb48e2675d97dab83b546fce0a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 16:37:40 +0100 Subject: [PATCH 23/42] Use feature flag in DuckURLSchemeHandler --- .../WKWebViewConfigurationExtensions.swift | 5 +- .../Utilities/UserDefaultsWrapper.swift.orig | 416 ++++++++++++++++++ .../YoutubePlayer/DuckURLSchemeHandler.swift | 16 +- .../DuckSchemeHandlerTests.swift | 17 +- 4 files changed, 446 insertions(+), 8 deletions(-) create mode 100644 DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig diff --git a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift index 9a89e51e4f..5ecc8aac06 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewConfigurationExtensions.swift @@ -60,7 +60,10 @@ extension WKWebViewConfiguration { if SupportedOSChecker.isCurrentOSReceivingUpdates { if urlSchemeHandler(forURLScheme: URL.NavigationalScheme.duck.rawValue) == nil { - setURLSchemeHandler(DuckURLSchemeHandler(), forURLScheme: URL.NavigationalScheme.duck.rawValue) + setURLSchemeHandler( + DuckURLSchemeHandler(featureFlagger: NSApp.delegateTyped.featureFlagger), + forURLScheme: URL.NavigationalScheme.duck.rawValue + ) } } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig new file mode 100644 index 0000000000..a8c2d4f76e --- /dev/null +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig @@ -0,0 +1,416 @@ +// +// UserDefaultsWrapper.swift +// +// Copyright © 2021 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 AppKit +import Foundation +import AppKitExtensions + +extension UserDefaults { + /// The app group's shared UserDefaults + static let netP = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .netP))! + static let dbp = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .dbp))! + static let subs = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .subs))! + static let appConfiguration = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .appConfiguration))! +} + +public struct UserDefaultsWrapperKey: RawRepresentable { + public let rawValue: String + public init(rawValue: String) { + self.rawValue = rawValue + } +} + +@propertyWrapper +public struct UserDefaultsWrapper { + + public typealias DefaultsKey = UserDefaultsWrapperKey + + public enum Key: String, CaseIterable { + /// system setting defining window title double-click action + case appleActionOnDoubleClick = "AppleActionOnDoubleClick" + + case fireproofDomains = "com.duckduckgo.fireproofing.allowedDomains" + case areDomainsMigratedToETLDPlus1 = "com.duckduckgo.are-domains-migrated-to-etldplus1" + case unprotectedDomains = "com.duckduckgo.contentblocker.unprotectedDomains" + case contentBlockingRulesCache = "com.duckduckgo.contentblocker.rules.cache" + + case defaultBrowserDismissed = "browser.default.dismissed" + + case spellingCheckEnabledOnce = "spelling.check.enabled.once" + case grammarCheckEnabledOnce = "grammar.check.enabled.once" + + case loginDetectionEnabled = "fireproofing.login-detection-enabled" + case autoClearEnabled = "preferences.auto-clear-enabled" + case warnBeforeClearingEnabled = "preferences.warn-before-clearing-enabled" + case gpcEnabled = "preferences.gpc-enabled" + case selectedDownloadLocationKey = "preferences.download-location" + case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location" + case alwaysRequestDownloadLocationKey = "preferences.download-location.always-request" + case openDownloadsPopupOnCompletionKey = "preferences.downloads.open.on.completion" + case autoconsentEnabled = "preferences.autoconsent-enabled" + case autoconsentFilterlistExperimentCohort = "preferences.autoconsent.filterListExperimentCohort" + case duckPlayerMode = "preferences.duck-player" + case youtubeOverlayInteracted = "preferences.youtube-overlay-interacted" + case youtubeOverlayButtonsUsed = "preferences.youtube-overlay-user-used-buttons" + case duckPlayerAutoplay = "preferences.duckplayer.autoplay" + case duckPlayerOpenInNewTab = "preferences.duckplayer.open-new-tab" + + case selectedPasswordManager = "preferences.autofill.selected-password-manager" + + case askToSaveUsernamesAndPasswords = "preferences.ask-to-save.usernames-passwords" + case askToSaveAddresses = "preferences.ask-to-save.addresses" + case askToSavePaymentMethods = "preferences.ask-to-save.payment-methods" + case autolockLocksFormFilling = "preferences.lock-autofill-form-fill" + case autofillDebugScriptEnabled = "preferences.enable-autofill-debug-script" + + case saveAsPreferredFileType = "saveAs.selected.filetype" + + case lastCrashReportCheckDate = "last.crash.report.check.date" + case didCrashDuringCrashHandlersSetUp = "browser.didCrashDuringCrashHandlersSetUp" + + case fireInfoPresentedOnce = "fire.info.presented.once" + case appTerminationHandledCorrectly = "app.termination.handled.correctly" + case restoreTabsOnStartup = "restore.tabs.on.startup" + + case restorePreviousSession = "preferences.startup.restore-previous-session" + case launchToCustomHomePage = "preferences.startup.launch-to-custom-home-page" + case customHomePageURL = "preferences.startup.customHomePageURL" + case currentThemeName = "com.duckduckgo.macos.currentThemeNameKey" + case showFullURL = "preferences.appearance.show-full-url" + case showAutocompleteSuggestions = "preferences.appearance.show-autocomplete-suggestions" + case preferNewTabsToWindows = "preferences.tabs.prefer-new-tabs-to-windows" + case switchToNewTabWhenOpened = "preferences.tabs.switch-to-new-tab-when-opened" + case newTabPosition = "preferences.tabs.new-tab-position" + case defaultPageZoom = "preferences.appearance.default-page-zoom" + case websitePageZoom = "preferences.appearance.website-page-zoom" + case bookmarksBarAppearance = "preferences.appearance.bookmarks-bar" + + case homeButtonPosition = "preferences.appeareance.home-button-position" + + case phishingDetectionEnabled = "preferences.security.phishing-detection-enabled" + + // ATB + case installDate = "statistics.installdate.key" + case atb = "statistics.atb.key" + case searchRetentionAtb = "statistics.retentionatb.key" + case appRetentionAtb = "statistics.appretentionatb.key" + case lastAppRetentionRequestDate = "statistics.appretentionatb.last.request.key" + + // Used to detect whether a user had old User Defaults ATB data at launch, in order to grant them implicitly + // unlocked status with regards to the lock screen + case legacyStatisticsStoreDataCleared = "statistics.appretentionatb.legacy-data-cleared" + + case onboardingFinished = "onboarding.finished" + case contextualOnboardingState = "contextual.onboarding.state" + + // Home Page + case homePageShowPagesOnHover = "home.page.show.pages.on.hover" + case homePageShowAllFavorites = "home.page.show.all.favorites" + case homePageShowAllFeatures = "home.page.show.all.features" + case homePageShowMakeDefault = "home.page.show.make.default" + case homePageShowAddToDock = "home.page.show.add.to.dock" + case homePageShowImport = "home.page.show.import" + case homePageShowDuckPlayer = "home.page.show.duck.player" + case homePageShowEmailProtection = "home.page.show.email.protection" + case homePageShowPageTitles = "home.page.show.page.titles" + case homePageShowRecentlyVisited = "home.page.show.recently.visited" + case homePageContinueSetUpImport = "home.page.continue.set.up.import" + case homePageIsFavoriteVisible = "home.page.is.favorite.visible" + case homePageIsContinueSetupVisible = "home.page.is.continue.setup.visible" + case homePageIsRecentActivityVisible = "home.page.is.recent.activity.visible" + case homePageIsSearchBarVisible = "home.page.is.search.bar.visible" + case homePageIsFirstSession = "home.page.is.first.session" + case homePageDidShowSettingsOnboarding = "home.page.did.show.settings.onboarding" + case homePageUserBackgroundImages = "home.page.user.background.images" + case homePageCustomBackground = "home.page.custom.background" + case homePageLastPickedCustomColor = "home.page.last.picked.custom.color" + + case homePagePromotionVisible = "home.page.promotion.visible" + case homePagePromotionDidDismiss = "home.page.promotion.did.dismiss" + + case appIsRelaunchingAutomatically = "app-relaunching-automatically" + + case historyV5toV6Migration = "history.v5.to.v6.migration.2" + case emailKeychainMigration = "email.keychain.migration" + + case bookmarksBarPromptShown = "bookmarks.bar.prompt.shown" + case showBookmarksBar = "bookmarks.bar.show" + case centerAlignedBookmarksBar = "bookmarks.bar.center.aligned" + case lastBookmarksBarUsagePixelSendDate = "bookmarks.bar.last-usage-pixel-send-date" + + case pinnedViews = "pinning.pinned-views" + case manuallyToggledPinnedViews = "pinning.manually-toggled-pinned-views" + + case lastDatabaseFactoryFailurePixelDate = "last.database.factory.failure.pixel.date" + + case loggingEnabledDate = "logging.enabled.date" + case loggingCategories = "logging.categories" + + case firstLaunchDate = "first.app.launch.date" + case customConfigurationUrl = "custom.configuration.url" + + case lastRemoteMessagingRefreshDate = "last.remote.messaging.refresh.date" + + // Data Broker Protection + + case dataBrokerProtectionTermsAndConditionsAccepted = "data-broker-protection.waitlist-terms-and-conditions.accepted" + case shouldShowDBPWaitlistInvitedCardUI = "shouldShowDBPWaitlistInvitedCardUI" + + // VPN + + case networkProtectionExcludedRoutes = "netp.excluded-routes" + + // VPN: Shared Defaults + // --- + // Please note that shared defaults MUST have a name that matches exactly their value, + // or else KVO will just not work as of 2023-08-07 + + case networkProtectionOnboardingStatusRawValue = "networkProtectionOnboardingStatusRawValue" + + // Updates + case automaticUpdates = "updates.automatic" + case pendingUpdateShown = "pending.update.shown" + + // Experiments + case pixelExperimentInstalled = "pixel.experiment.installed" + case pixelExperimentCohort = "pixel.experiment.cohort" + case pixelExperimentEnrollmentDate = "pixel.experiment.enrollment.date" + case pixelExperimentFiredPixels = "pixel.experiment.pixels.fired" + case campaignVariant = "campaign.variant" + + // Updates + case previousAppVersion = "previous.app.version" + case previousBuild = "previous.build" + + // Sync + + case syncEnvironment = "sync.environment" + case favoritesDisplayMode = "sync.favorites-display-mode" + case syncBookmarksPaused = "sync.bookmarks-paused" + case syncCredentialsPaused = "sync.credentials-paused" + case syncIsPaused = "sync.paused" + case syncBookmarksPausedErrorDisplayed = "sync.bookmarks-paused-error-displayed" + case syncCredentialsPausedErrorDisplayed = "sync.credentials-paused-error-displayed" + case syncInvalidLoginPausedErrorDisplayed = "sync.invalid-login-paused-error-displayed" + case syncIsFaviconsFetcherEnabled = "sync.is-favicons-fetcher-enabled" + case syncIsEligibleForFaviconsFetcherOnboarding = "sync.is-eligible-for-favicons-fetcher-onboarding" + case syncDidPresentFaviconsFetcherOnboarding = "sync.did-present-favicons-fetcher-onboarding" + case syncDidMigrateToImprovedListsHandling = "sync.did-migrate-to-improved-lists-handling" + case syncDidShowSyncPausedByFeatureFlagAlert = "sync.did-show-sync-paused-by-feature-flag-alert" + case syncLastErrorNotificationTime = "sync.last-error-notification-time" + case syncLastSuccesfullTime = "sync.last-time-success" + case syncLastNonActionableErrorCount = "sync.non-actionable-error-count" + case syncCurrentAllPausedError = "sync.current-all-paused-error" + case syncCurrentBookmarksPausedError = "sync.current-bookmarks-paused-error" + case syncCurrentCredentialsPausedError = "sync.current-credentials-paused-error" + case syncPromoBookmarksDismissed = "sync.promotion-bookmarks-dismissed" + case syncPromoPasswordsDismissed = "sync.promotion-passwords-dismissed" + + // Subscription + + case subscriptionEnvironment = "subscription.environment" + +<<<<<<< HEAD + // Experimental Feature Flags + + case htmlNewTabPage = "experimental.html-new-tab-page" +======= + // PageRefreshMonitor + + case refreshTimestamps = "pageRefreshMonitor.refresh-timestamps" + + // BrokenSitePrompt + + case lastBrokenSiteToastShownDate = "brokenSitePrompt.last-broken-site-toast-shown-date" + case toastDismissStreakCounter = "brokenSitePrompt.toast-dismiss-streak-counter" + +>>>>>>> main + } + + enum RemovedKeys: String, CaseIterable { + case passwordManagerDoNotPromptDomains = "com.duckduckgo.passwordmanager.do-not-prompt-domains" + case incrementalFeatureFlagTestHasSentPixel = "network-protection.incremental-feature-flag-test.has-sent-pixel" + case homePageShowNetworkProtectionBetaEndedNotice = "home.page.network-protection.show-beta-ended-notice" + + // NetP removed keys + case networkProtectionShouldEnforceRoutes = "netp.enforce-routes" + case networkProtectionShouldIncludeAllNetworks = "netp.include-all-networks" + case networkProtectionConnectionTesterEnabled = "netp.connection-tester-enabled" + case networkProtectionShouldExcludeLocalNetworks = "netp.exclude-local-routes" + case networkProtectionRegistrationKeyValidity = "com.duckduckgo.network-protection.NetworkProtectionTunnelController.registrationKeyValidityKey" + case shouldShowNetworkProtectionSystemExtensionUpgradePrompt = "network-protection.show-system-extension-upgrade-prompt" + } + + private let key: DefaultsKey + private let getter: (Any?) -> T + private let setter: (UserDefaultsWrapper, T) -> Void + + private let customUserDefaults: UserDefaults? + + var defaults: UserDefaults { + customUserDefaults ?? Self.sharedDefaults + } + + static var sharedDefaults: UserDefaults { +#if DEBUG && !(NETP_SYSTEM_EXTENSION && NETWORK_EXTENSION) // Avoid looking up special user defaults when running inside the system extension + if case .normal = NSApplication.runType { + return .standard + } else { + return UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! + } +#else + return .standard +#endif + } + + public init(key: DefaultsKey, defaults: UserDefaults? = nil, getter: @escaping (Any?) -> T, setter: @escaping (UserDefaultsWrapper, T) -> Void) { + self.key = key + self.getter = getter + self.setter = setter + self.customUserDefaults = defaults + } + + @_disfavoredOverload + public init(key: DefaultsKey, defaultValue: T, defaults: UserDefaults? = nil) { + @inline(__always) func isNil(_ value: T) -> Bool { + @inline(__always) func cast(_ value: V, to _: R.Type) -> R? { + value as? R // perform `value as? Any?` suppressing `casting to Any? always succeeds` warning + } + return if case .some(.none) = cast(value, to: Any?.self) { true } else { false } + } + + self.init(key: key, defaults: defaults) { + guard let value = $0 as? T, !isNil(value) else { return defaultValue } + return value + } setter: { this, newValue in + guard PropertyListSerialization.propertyList(newValue, isValidFor: .binary) else { + if isNil(newValue) { + this.defaults.removeObject(forKey: key.rawValue) + return + } + assertionFailure("\(newValue) cannot be stored in UserDefaults") + return + } + this.defaults.set(newValue, forKey: key.rawValue) + } + } + + public init(key: DefaultsKey, defaults: UserDefaults? = nil) where T == Wrapped? { + self.init(key: key, defaults: defaults) { + $0 as? Wrapped + } setter: { this, newValue in + guard let newValue else { + this.defaults.removeObject(forKey: key.rawValue) // newValue is nil + return + } + guard PropertyListSerialization.propertyList(newValue, isValidFor: .binary) else { + assertionFailure("\(newValue) cannot be stored in UserDefaults") + return + } + this.defaults.set(newValue, forKey: key.rawValue) + } + } + + @available(*, unavailable, message: "Cannot use overload with `defaultValue` for an Optional Value") + public init(key: DefaultsKey, defaultValue: Wrapped, defaults: UserDefaults? = nil) where T == Wrapped? { + fatalError() + } + + public init(key: DefaultsKey, defaultValue: T, defaults: UserDefaults? = nil) where T: RawRepresentable { + self.init(key: key, defaults: defaults) { + ($0 as? RawValue).flatMap(T.init(rawValue:)) ?? defaultValue + } setter: { this, newValue in + guard PropertyListSerialization.propertyList(newValue.rawValue, isValidFor: .binary) else { + assertionFailure("\(newValue.rawValue) cannot be stored in UserDefaults") + return + } + this.defaults.set(newValue.rawValue, forKey: key.rawValue) + } + } + + public init(key: DefaultsKey, defaults: UserDefaults? = nil) where T == Wrapped?, Wrapped: RawRepresentable { + self.init(key: key, defaults: defaults) { + ($0 as? RawValue).flatMap(Wrapped.init(rawValue:)) + } setter: { this, newValue in + guard let newValue else { + this.defaults.removeObject(forKey: key.rawValue) // newValue is nil + return + } + guard PropertyListSerialization.propertyList(newValue.rawValue, isValidFor: .binary) else { + assertionFailure("\(newValue.rawValue) cannot be stored in UserDefaults") + return + } + this.defaults.set(newValue.rawValue, forKey: key.rawValue) + } + } + + @_disfavoredOverload + public init(key: Key, defaultValue: T, defaults: UserDefaults? = nil) { + self.init(key: .init(rawValue: key.rawValue), defaultValue: defaultValue, defaults: defaults) + } + + public init(key: Key, defaults: UserDefaults? = nil) where T == Wrapped? { + self.init(key: .init(rawValue: key.rawValue), defaults: defaults) + } + + public init(key: Key, defaultValue: T, defaults: UserDefaults? = nil) where T: RawRepresentable { + self.init(key: .init(rawValue: key.rawValue), defaultValue: defaultValue, defaults: defaults) + } + + public init(key: Key, defaults: UserDefaults? = nil) where T == Wrapped?, Wrapped: RawRepresentable { + self.init(key: .init(rawValue: key.rawValue), defaults: defaults) + } + + public var wrappedValue: T { + get { + let storedValue = defaults.object(forKey: key.rawValue) + return getter(storedValue) + } + nonmutating set { + setter(self, newValue) + } + } + + static func clearAll() { + let defaults = sharedDefaults + Key.allCases.forEach { key in + defaults.removeObject(forKey: key.rawValue) + } + } + + static func clearRemovedKeys() { + let defaults = sharedDefaults + RemovedKeys.allCases.forEach { key in + defaults.removeObject(forKey: key.rawValue) + } + } + + func clear() { + defaults.removeObject(forKey: key.rawValue) + } + +} + +extension UserDefaultsWrapper where T == Any { + static func clear(_ key: Key) { + sharedDefaults.removeObject(forKey: key.rawValue) + } + static func clear(_ key: DefaultsKey) { + sharedDefaults.removeObject(forKey: key.rawValue) + } +} diff --git a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift index aac4abfb3f..acd03cc599 100644 --- a/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/YoutubePlayer/DuckURLSchemeHandler.swift @@ -16,6 +16,8 @@ // limitations under the License. // +import BrowserServicesKit +import FeatureFlags import Foundation import WebKit import ContentScopeScripts @@ -23,6 +25,12 @@ import PhishingDetection final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { + let featureFlagger: FeatureFlagger + + init(featureFlagger: FeatureFlagger) { + self.featureFlagger = featureFlagger + } + func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { guard let requestURL = webView.url ?? urlSchemeTask.request.url else { assertionFailure("No URL for Duck scheme handler") @@ -30,12 +38,18 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { } switch requestURL.type { - case .onboarding, .releaseNotes, .newTab: + case .onboarding, .releaseNotes: handleSpecialPages(urlSchemeTask: urlSchemeTask) case .duckPlayer: handleDuckPlayer(requestURL: requestURL, urlSchemeTask: urlSchemeTask, webView: webView) case .phishingErrorPage: handleErrorPage(urlSchemeTask: urlSchemeTask) + case .newTab: + if featureFlagger.isFeatureOn(.htmlNewTabPage) { + handleSpecialPages(urlSchemeTask: urlSchemeTask) + } else { + handleNativeUIPages(requestURL: requestURL, urlSchemeTask: urlSchemeTask) + } default: handleNativeUIPages(requestURL: requestURL, urlSchemeTask: urlSchemeTask) } diff --git a/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift b/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift index 7e28a5335c..32b6109bff 100644 --- a/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift +++ b/UnitTests/DuckSchemeHandler/DuckSchemeHandlerTests.swift @@ -25,10 +25,20 @@ import PhishingDetection final class DuckSchemeHandlerTests: XCTestCase { + var featureFlagger: MockFeatureFlagger! + var handler: DuckURLSchemeHandler! + + override func setUp() { + super.setUp() + featureFlagger = MockFeatureFlagger() + featureFlagger.isFeatureOn = false + + handler = DuckURLSchemeHandler(featureFlagger: featureFlagger) + } + func testWebViewFromOnboardingHandlerReturnsResponseAndData() throws { // Given let onboardingURL = URL(string: "duck://onboarding")! - let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: onboardingURL)) @@ -47,7 +57,6 @@ final class DuckSchemeHandlerTests: XCTestCase { func testWebViewFromReleaseNoteHandlerReturnsResponseAndData() throws { // Given let releaseNotesURL = URL(string: "duck://release-notes")! - let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: releaseNotesURL)) @@ -67,7 +76,6 @@ final class DuckSchemeHandlerTests: XCTestCase { func testWebViewFromDuckPlayerHandlerReturnsResponseAndData() throws { // Given let duckPlayerURL = URL(string: "duck://player")! - let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: duckPlayerURL)) @@ -84,7 +92,6 @@ final class DuckSchemeHandlerTests: XCTestCase { func testWebViewFromNativeUIHandlerReturnsResponseAndData() throws { // Given let nativeURL = URL(string: "duck://newtab")! - let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: nativeURL)) @@ -123,7 +130,6 @@ final class DuckSchemeHandlerTests: XCTestCase { let token = URLTokenValidator.shared.generateToken(for: phishingUrl) let errorURLString = "duck://error?reason=phishing&url=\(encodedURL)&token=\(token)" let errorURL = URL(string: errorURLString)! - let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: errorURL)) @@ -148,7 +154,6 @@ final class DuckSchemeHandlerTests: XCTestCase { let token = "ababababababababababab" let errorURLString = "duck://error?reason=phishing&url=\(encodedURL)&token=\(token)" let errorURL = URL(string: errorURLString)! - let handler = DuckURLSchemeHandler() let webView = WKWebView() let schemeTask = MockSchemeTask(request: URLRequest(url: errorURL)) From 1396f7b47d7ed5b7eddb298fc3def06e74e42d7e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 16:48:37 +0100 Subject: [PATCH 24/42] FeatureFlagProtocol -> FeatureFlagDescribing --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 2 +- DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift | 2 +- .../PhishingDetection/PhishingDetectionIntegrationTests.swift | 2 +- .../FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift | 2 +- UnitTests/Menus/MainMenuTests.swift | 2 +- UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fcab3f31c6..9095331c90 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,7 +33,7 @@ "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { "branch" : "dominik/feature-flag-overrides", - "revision" : "ba54e169a4db0633864b56190cb7129baed3521a" + "revision" : "5be16267af6b40dca2f8aa2ff4559957f4368c77" } }, { diff --git a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift index 09171daaff..a8d58dbe44 100644 --- a/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift +++ b/DuckDuckGo/InternalUserDecider/FeatureFlagOverridesMenu.swift @@ -21,7 +21,7 @@ import BrowserServicesKit import FeatureFlags struct FeatureFlagOverridesDefaultHandler: FeatureFlagLocalOverridesHandler { - func flagDidChange(_ featureFlag: Flag, isEnabled: Bool) { + func flagDidChange(_ featureFlag: Flag, isEnabled: Bool) { guard let flag = featureFlag as? FeatureFlag else { return } switch flag { case .htmlNewTabPage: diff --git a/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift b/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift index 7b1bcfd89e..0080b3fd6d 100644 --- a/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift +++ b/IntegrationTests/PhishingDetection/PhishingDetectionIntegrationTests.swift @@ -151,7 +151,7 @@ class MockFeatureFlagger: FeatureFlagger { var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: MockInternalUserStoring()) var localOverrides: FeatureFlagLocalOverriding? - func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { + func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { return true } } diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift index c7f9ba5d9d..59725f11e5 100644 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift +++ b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift @@ -48,7 +48,7 @@ public enum FeatureFlag: String, CaseIterable { case htmlNewTabPage } -extension FeatureFlag: FeatureFlagProtocol { +extension FeatureFlag: FeatureFlagDescribing { public var supportsLocalOverriding: Bool { switch self { case .htmlNewTabPage: diff --git a/UnitTests/Menus/MainMenuTests.swift b/UnitTests/Menus/MainMenuTests.swift index 59cde2b7e2..73f48a53e5 100644 --- a/UnitTests/Menus/MainMenuTests.swift +++ b/UnitTests/Menus/MainMenuTests.swift @@ -197,7 +197,7 @@ private class DummyFeatureFlagger: FeatureFlagger { var internalUserDecider: InternalUserDecider = DefaultInternalUserDecider(store: MockInternalUserStoring()) var localOverrides: FeatureFlagLocalOverriding? - func isFeatureOn(for: Flag, allowOverride: Bool) -> Bool { + func isFeatureOn(for: Flag, allowOverride: Bool) -> Bool { false } } diff --git a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift index 14e82673f2..dbfad1313c 100644 --- a/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift +++ b/UnitTests/TabExtensionsTests/ErrorPageTabExtensionTest.swift @@ -484,7 +484,7 @@ class MockFeatureFlagger: FeatureFlagger { var localOverrides: FeatureFlagLocalOverriding? var isFeatureOn = true - func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { + func isFeatureOn(for featureFlag: Flag, allowOverride: Bool) -> Bool { return isFeatureOn } } From 83ba686d0df89efb9f232dcb444b144d7ec0da6d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 21:25:21 +0100 Subject: [PATCH 25/42] Revert "PoC RMF support" This reverts commit ee77f47b8c073415864b8eb85823fd84c33a8e79. --- DuckDuckGo/Application/AppDelegate.swift | 7 +- .../HomePage/NewTabPageActionsManager.swift | 23 +---- .../HomePage/NewTabPageUserScript.swift | 88 +------------------ .../SpecialPagesUserScriptExtension.swift | 5 ++ 4 files changed, 12 insertions(+), 111 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index b1935a87a1..2ea5b201a2 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -44,6 +44,8 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { + let newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared) + private(set) lazy var newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) let experimentalFeatures = ExperimentalFeatures() @objc func toggleHTMLNTP(_ sender: NSMenuItem) { @@ -96,8 +98,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? - let newTabPageActionsManager: NewTabPageActionsManaging - let newTabPageUserScript: NewTabPageUserScript let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! @@ -305,9 +305,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) - - newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared, activeRemoteMessageModel: activeRemoteMessageModel) - newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index efaa16dafb..0ac75964fd 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -19,7 +19,6 @@ import Foundation import Combine import PixelKit -import RemoteMessaging import Common import os.log @@ -31,7 +30,6 @@ protocol NewTabPageActionsManaging: AnyObject { func reportException(with params: [String: String]) func showContextMenu(with params: [String: Any]) func updateWidgetConfigs(with params: [[String: String]]) - func getRemoteMessage() -> NTP.RMFMessage? } struct NewTabPageConfiguration: Encodable { @@ -72,14 +70,11 @@ struct NewTabPageConfiguration: Encodable { final class NewTabPageActionsManager: NewTabPageActionsManaging { private let appearancePreferences: AppearancePreferences - private let activeRemoteMessageModel: ActiveRemoteMessageModel - private var cancellables = Set() weak var userScript: NewTabPageUserScript? - init(appearancePreferences: AppearancePreferences, activeRemoteMessageModel: ActiveRemoteMessageModel) { + init(appearancePreferences: AppearancePreferences) { self.appearancePreferences = appearancePreferences - self.activeRemoteMessageModel = activeRemoteMessageModel appearancePreferences.$isFavoriteVisible.dropFirst().removeDuplicates().asVoid() .receive(on: DispatchQueue.main) @@ -94,25 +89,15 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { self?.notifyWidgetConfigsDidChange() } .store(in: &cancellables) - - activeRemoteMessageModel.$remoteMessage.dropFirst() - .sink { [weak self] remoteMessage in - self?.notifyRemoteMessageDidChange(remoteMessage) - } - .store(in: &cancellables) } private func notifyWidgetConfigsDidChange() { - userScript?.notifyWidgetConfigsDidChange(widgetConfigs: [ + userScript?.widgetConfigsUpdated(widgetConfigs: [ .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) ]) } - private func notifyRemoteMessageDidChange(_ remoteMessage: RemoteMessageModel?) { - userScript?.notifyRemoteMessageDidChange(.small(.init(descriptionText: "Hello, this is a description", id: "hejka", titleText: "Hello I'm a title"))) - } - var configuration: NewTabPageConfiguration { #if DEBUG || REVIEW let env = "development" @@ -194,10 +179,6 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { } } - func getRemoteMessage() -> NTP.RMFMessage? { - .small(.init(descriptionText: "Hello, this is a description", id: "hejka", titleText: "Hello I'm a title")) - } - func reportException(with params: [String: String]) { let message = params["message"] ?? "" let id = params["id"] ?? "" diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index b6619234e8..a1429dc909 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -34,7 +34,6 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { case initialSetup case reportInitException case reportPageException - case rmfGetData = "rmf_getData" case widgetsSetConfig = "widgets_setConfig" } @@ -53,7 +52,6 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { .initialSetup: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, .reportInitException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, .reportPageException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, - .rmfGetData: { [weak self] in try await self?.rmfGetData(params: $0, original: $1) }, .widgetsSetConfig: { [weak self] in try await self?.widgetsSetConfig(params: $0, original: $1) } ] @@ -63,24 +61,17 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { return methodHandlers[messageName] } - func notifyWidgetConfigsDidChange(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { + func widgetConfigsUpdated(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { for webView in webViews.allObjects { broker?.push(method: "widgets_onConfigUpdated", params: widgetConfigs, for: self, into: webView) } } - - func notifyRemoteMessageDidChange(_ remoteMessage: NTP.RMFMessage?) { - let data = NTP.RMFData(content: remoteMessage) - for webView in webViews.allObjects { - broker?.push(method: "rmf_onDataUpdate", params: data, for: self, into: webView) - } - } } extension NewTabPageUserScript { @MainActor private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { - actionsManager.configuration + return actionsManager.configuration } @MainActor @@ -97,85 +88,12 @@ extension NewTabPageUserScript { return nil } - @MainActor - private func rmfGetData(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let data = NTP.RMFData(content: actionsManager.getRemoteMessage()) - return data - } - private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [String: String] else { return nil } actionsManager.reportException(with: params) return nil } -} + struct Result: Encodable {} -enum NTP { - struct RMFData: Encodable { - var content: RMFMessage? - } - - enum RMFMessage: Encodable { - case small(SmallMessage), medium(MediumMessage), bigSingleAction(BigSingleActionMessage), bigTwoAction(BigTwoActionMessage) - - func encode(to encoder: any Encoder) throws { - try message.encode(to: encoder) - } - - var message: Encodable { - switch self { - case .small(let message): - return message - case .medium(let message): - return message - case .bigSingleAction(let message): - return message - case .bigTwoAction(let message): - return message - } - } - } - - struct SmallMessage: Encodable { - let messageType = "small" - - var descriptionText: String - var id: String - var titleText: String - } - - struct MediumMessage: Encodable { - let messageType = "medium" - - var descriptionText: String - var icon: RMFIcon - var id: String - var titleText: String - } - - struct BigSingleActionMessage: Encodable { - let messageType = "big_single_action" - - var descriptionText: String - var icon: RMFIcon - var id: String - var primaryActionText: String - var titleText: String - } - - struct BigTwoActionMessage: Encodable { - let messageType = "big_two_action" - - var descriptionText: String - var icon: RMFIcon - var id: String - var primaryActionText: String - var secondaryActionText: String - var titleText: String - } - - enum RMFIcon: String, Encodable { - case announce, ddgAnnounce, criticalUpdate, appUpdate, privacyPro - } } diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index 6c3ead0f1b..473cddc5dd 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -66,4 +66,9 @@ extension SpecialPagesUserScript { appearancePreferences: AppearancePreferences.shared, startupPreferences: StartupPreferences.shared) } + + @MainActor + private func buildNewTabPageActionsManager() -> NewTabPageActionsManaging { + NewTabPageActionsManager(appearancePreferences: .shared) + } } From fbebacbf4a21e60d3bd874ab905f51e95abd432f Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 22:33:48 +0100 Subject: [PATCH 26/42] Clean up code --- DuckDuckGo/Application/AppDelegate.swift | 8 +++++--- DuckDuckGo/HomePage/NewTabPageActionsManager.swift | 2 +- DuckDuckGo/HomePage/NewTabPageUserScript.swift | 6 ++---- .../Tab/Model/SpecialPagesUserScriptExtension.swift | 5 ----- DuckDuckGo/Tab/View/BrowserTabViewController.swift | 2 +- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 6b245da669..75f6780080 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -45,9 +45,6 @@ import Freemium final class AppDelegate: NSObject, NSApplicationDelegate { - let newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared) - private(set) lazy var newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) - #if DEBUG let disableCVDisplayLinkLogs: Void = { // Disable CVDisplayLink logs @@ -95,6 +92,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let bookmarksManager = LocalBookmarkManager.shared var privacyDashboardWindow: NSWindow? + let newTabPageActionsManager: NewTabPageActionsManaging + let newTabPageUserScript: NewTabPageUserScript let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! @@ -311,6 +310,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPUserStateManager: freemiumDBPUserStateManager) freemiumDBPPromotionViewCoordinator = FreemiumDBPPromotionViewCoordinator(freemiumDBPUserStateManager: freemiumDBPUserStateManager, freemiumDBPFeature: freemiumDBPFeature) + + newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared) + newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index 0ac75964fd..2f89302331 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -92,7 +92,7 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { } private func notifyWidgetConfigsDidChange() { - userScript?.widgetConfigsUpdated(widgetConfigs: [ + userScript?.notifyWidgetConfigsDidChange(widgetConfigs: [ .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) ]) diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index a1429dc909..0eef00b9d5 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -61,7 +61,7 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { return methodHandlers[messageName] } - func widgetConfigsUpdated(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { + func notifyWidgetConfigsDidChange(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { for webView in webViews.allObjects { broker?.push(method: "widgets_onConfigUpdated", params: widgetConfigs, for: self, into: webView) } @@ -71,7 +71,7 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { extension NewTabPageUserScript { @MainActor private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { - return actionsManager.configuration + actionsManager.configuration } @MainActor @@ -94,6 +94,4 @@ extension NewTabPageUserScript { return nil } - struct Result: Encodable {} - } diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index 473cddc5dd..6c3ead0f1b 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -66,9 +66,4 @@ extension SpecialPagesUserScript { appearancePreferences: AppearancePreferences.shared, startupPreferences: StartupPreferences.shared) } - - @MainActor - private func buildNewTabPageActionsManager() -> NewTabPageActionsManaging { - NewTabPageActionsManager(appearancePreferences: .shared) - } } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index daa83aab0c..e7429d0296 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -48,6 +48,7 @@ final class BrowserTabViewController: NSViewController { let webView = WebView(frame: .zero, configuration: configuration) NSApp.delegateTyped.newTabPageUserScript.webViews.add(webView) + webView.load(URLRequest(url: URL.newtab)) return webView }() @@ -149,7 +150,6 @@ final class BrowserTabViewController: NSViewController { } view.registerForDraggedTypes([.URL, .fileURL]) - newTabPageWebView.load(URLRequest(url: URL.newtab)) } @objc func windowDidBecomeActive(notification: Notification) { From 779379ac9a590fa347cd344a6af3e5b9075d0deb Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 22:40:50 +0100 Subject: [PATCH 27/42] Use local featureFlagger property and pass newTabPageUserScript in BrowserTabViewController initializer --- DuckDuckGo/Tab/View/BrowserTabViewController.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index e7429d0296..8ca229fe52 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -41,13 +41,14 @@ final class BrowserTabViewController: NSViewController { private lazy var hoverLabel = NSTextField(string: URL.duckDuckGo.absoluteString) private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) + private let newTabPageUserScript: NewTabPageUserScript private lazy var newTabPageWebView: WebView = { let configuration = WKWebViewConfiguration() configuration.applyNTPConfiguration() let webView = WebView(frame: .zero, configuration: configuration) - NSApp.delegateTyped.newTabPageUserScript.webViews.add(webView) + newTabPageUserScript.webViews.add(webView) webView.load(URLRequest(url: URL.newtab)) return webView @@ -94,12 +95,15 @@ final class BrowserTabViewController: NSViewController { bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, onboardingDialogTypeProvider: ContextualOnboardingDialogTypeProviding & ContextualOnboardingStateUpdater = Application.appDelegate.onboardingStateMachine, onboardingDialogFactory: ContextualDaxDialogsFactory = DefaultContextualDaxDialogViewFactory(), - featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger) { + featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger, + newTabPageUserScript: NewTabPageUserScript = NSApp.delegateTyped.newTabPageUserScript + ) { self.tabCollectionViewModel = tabCollectionViewModel self.bookmarkManager = bookmarkManager self.onboardingDialogTypeProvider = onboardingDialogTypeProvider self.onboardingDialogFactory = onboardingDialogFactory self.featureFlagger = featureFlagger + self.newTabPageUserScript = newTabPageUserScript containerStackView = NSStackView() super.init(nibName: nil, bundle: nil) @@ -789,7 +793,7 @@ final class BrowserTabViewController: NSViewController { updateTabIfNeeded(tabViewModel: tabViewModel) case .newtab: - if NSApp.delegateTyped.featureFlagger.isFeatureOn(.htmlNewTabPage) { + if featureFlagger.isFeatureOn(.htmlNewTabPage) { updateTabIfNeeded(tabViewModel: tabViewModel) } else { removeAllTabContent() @@ -859,7 +863,7 @@ final class BrowserTabViewController: NSViewController { case .onboarding: return case .newtab: - containsHostingView = !NSApp.delegateTyped.featureFlagger.isFeatureOn(.htmlNewTabPage) + containsHostingView = !featureFlagger.isFeatureOn(.htmlNewTabPage) case .settings: containsHostingView = true default: From 8dd5ab92416d8910f01953c2bf49855f641910f1 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 22:48:00 +0100 Subject: [PATCH 28/42] Clean up code --- .../NewTabPageUserContentController.swift | 21 +++++++++---------- .../Tab/View/BrowserTabViewController.swift | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift index 5413793138..daa6ee35e7 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift @@ -23,15 +23,15 @@ import UserScript final class NewTabPageUserContentController: WKUserContentController { - let ntpUserScripts: NTPUserScript + let newTabPageUserScriptProvider: NewTabPageUserScriptProvider @MainActor override init() { - ntpUserScripts = NTPUserScript() + newTabPageUserScriptProvider = NewTabPageUserScriptProvider() super.init() - ntpUserScripts.userScripts.forEach { + newTabPageUserScriptProvider.userScripts.forEach { let userScript = $0.makeWKUserScriptSync() self.installUserScripts([userScript], handlers: [$0]) } @@ -49,14 +49,14 @@ final class NewTabPageUserContentController: WKUserContentController { } @MainActor -final class NTPUserScript: UserScriptsProvider { - lazy var userScripts: [UserScript] = [specialPagesUserScriptIsolated] +final class NewTabPageUserScriptProvider: UserScriptsProvider { + lazy var userScripts: [UserScript] = [specialPagesUserScript] - let specialPagesUserScriptIsolated: SpecialPagesUserScript + let specialPagesUserScript: SpecialPagesUserScript init() { - specialPagesUserScriptIsolated = SpecialPagesUserScript() - specialPagesUserScriptIsolated.withNewTabPage() + specialPagesUserScript = SpecialPagesUserScript() + specialPagesUserScript.withNewTabPage() } @MainActor @@ -80,11 +80,10 @@ final class NTPUserScript: UserScriptsProvider { extension WKWebViewConfiguration { @MainActor - func applyNTPConfiguration() { - preferences.isFraudulentWebsiteWarningEnabled = false + func applyNewTabPageWebViewConfiguration(with featureFlagger: FeatureFlagger) { if urlSchemeHandler(forURLScheme: URL.NavigationalScheme.duck.rawValue) == nil { setURLSchemeHandler( - DuckURLSchemeHandler(featureFlagger: NSApp.delegateTyped.featureFlagger), + DuckURLSchemeHandler(featureFlagger: featureFlagger), forURLScheme: URL.NavigationalScheme.duck.rawValue ) } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 8ca229fe52..0145d02131 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -44,7 +44,7 @@ final class BrowserTabViewController: NSViewController { private let newTabPageUserScript: NewTabPageUserScript private lazy var newTabPageWebView: WebView = { let configuration = WKWebViewConfiguration() - configuration.applyNTPConfiguration() + configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger) let webView = WebView(frame: .zero, configuration: configuration) From 4a10dfaad0ff9ae69dbbdd38af25d8fdc0ca8f55 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 22:56:37 +0100 Subject: [PATCH 29/42] Clean up ReleaseNotesUserScript --- DuckDuckGo/Updates/ReleaseNotesUserScript.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift index 16e7dc7e02..620c2ff3b9 100644 --- a/DuckDuckGo/Updates/ReleaseNotesUserScript.swift +++ b/DuckDuckGo/Updates/ReleaseNotesUserScript.swift @@ -26,7 +26,7 @@ import Combine final class ReleaseNotesUserScript: NSObject, Subfeature { lazy var updateController: UpdateControllerProtocol = Application.appDelegate.updateController - var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes"), .exact(hostname: "newtab")]) + var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "release-notes")]) let featureName: String = "release-notes" weak var broker: UserScriptMessageBroker? weak var webView: WKWebView? { From e49f0beff66d76e02f1e28b75a6fed5aebea2e43 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 13 Nov 2024 23:38:31 +0100 Subject: [PATCH 30/42] Add placeholder empty Favorites --- .../HomePage/NewTabPageActionsManager.swift | 12 ++++ .../HomePage/NewTabPageUserScript.swift | 69 +++++++++++++++++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index 2f89302331..50650c258b 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -26,6 +26,8 @@ protocol NewTabPageActionsManaging: AnyObject { var configuration: NewTabPageConfiguration { get } var userScript: NewTabPageUserScript? { get set } + func getFavorites() -> NewTabPageUserScript.FavoritesData + func getFavoritesConfig() -> NewTabPageUserScript.FavoritesConfig /// It is called in case of error loading the pages func reportException(with params: [String: String]) func showContextMenu(with params: [String: Any]) @@ -120,6 +122,16 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { ) } + func getFavorites() -> NewTabPageUserScript.FavoritesData { + // implementation TBD + .init(favorites: []) + } + + func getFavoritesConfig() -> NewTabPageUserScript.FavoritesConfig { + // implementation TBD + .init(animation: .auto, expansion: .collapsed) + } + func showContextMenu(with params: [String: Any]) { guard let menuItems = params["visibilityMenuItems"] as? [[String: String]] else { return diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index 0eef00b9d5..c6beec16a3 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -31,6 +31,8 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { // MARK: - MessageNames enum MessageNames: String, CaseIterable { case contextMenu + case favoritesGetConfig = "favorites_getConfig" + case favoritesGetData = "favorites_getData" case initialSetup case reportInitException case reportPageException @@ -49,6 +51,8 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { private lazy var methodHandlers: [MessageNames: Handler] = [ .contextMenu: { [weak self] in try await self?.showContextMenu(params: $0, original: $1) }, + .favoritesGetConfig: { [weak self] in try await self?.favoritesGetConfig(params: $0, original: $1) }, + .favoritesGetData: { [weak self] in try await self?.favoritesGetData(params: $0, original: $1) }, .initialSetup: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, .reportInitException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, .reportPageException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, @@ -69,6 +73,23 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { } extension NewTabPageUserScript { + @MainActor + private func showContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let params = params as? [String: Any] else { return nil } + actionsManager.showContextMenu(with: params) + return nil + } + + @MainActor + private func favoritesGetConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + actionsManager.getFavoritesConfig() + } + + @MainActor + private func favoritesGetData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + actionsManager.getFavorites() + } + @MainActor private func initialSetup(params: Any, original: WKScriptMessage) async throws -> Encodable? { actionsManager.configuration @@ -81,17 +102,51 @@ extension NewTabPageUserScript { return nil } - @MainActor - private func showContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let params = params as? [String: Any] else { return nil } - actionsManager.showContextMenu(with: params) - return nil - } - private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [String: String] else { return nil } actionsManager.reportException(with: params) return nil } +} + +extension NewTabPageUserScript { + + struct FavoritesConfig: Encodable { + let animation: Animation? + let expansion: Expansion + } + + enum Expansion: String, Encodable { + case collapsed, expanded + } + + struct Animation: Encodable { + let kind: AnimationKind + + static let none = Animation(kind: .none) + static let viewTransitions = Animation(kind: .viewTransitions) + static let auto = Animation(kind: .auto) + + enum AnimationKind: String, Encodable { + case none + case viewTransitions = "view-transitions" + case auto = "auto-animate" + } + } + struct FavoritesData: Encodable { + var favorites: [Favorite] + } + + struct Favorite: Encodable { + let favicon: FavoriteFavicon? + let id: String + let title: String + let url: String + } + + struct FavoriteFavicon: Encodable { + var maxAvailableSize: Int + var src: String + } } From 58b2d02fcd93c347fbdd3dcc79eb909689181f89 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 00:12:53 +0100 Subject: [PATCH 31/42] Make developer tools work with the NTP webView --- .../HomePage/NewTabPageUserContentController.swift | 1 + DuckDuckGo/MainWindow/MainViewController.swift | 7 ++++++- DuckDuckGo/Menus/MainMenuActions.swift | 13 ++++++++----- DuckDuckGo/Tab/View/BrowserTabViewController.swift | 5 ++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift index daa6ee35e7..edc1e6e315 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift @@ -87,6 +87,7 @@ extension WKWebViewConfiguration { forURLScheme: URL.NavigationalScheme.duck.rawValue ) } + preferences[.developerExtrasEnabled] = true self.userContentController = NewTabPageUserContentController() } } diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index 7d8df888c9..39232b9ec6 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import BrowserServicesKit import Cocoa import Carbon.HIToolbox import Combine @@ -34,6 +35,7 @@ final class MainViewController: NSViewController { let findInPageViewController: FindInPageViewController let fireViewController: FireViewController let bookmarksBarViewController: BookmarksBarViewController + let featureFlagger: FeatureFlagger private let bookmarksBarVisibilityManager: BookmarksBarVisibilityManager let tabCollectionViewModel: TabCollectionViewModel @@ -63,12 +65,15 @@ final class MainViewController: NSViewController { autofillPopoverPresenter: AutofillPopoverPresenter, vpnXPCClient: VPNControllerXPCClient = .shared, aiChatMenuConfig: AIChatMenuVisibilityConfigurable = AIChatMenuConfiguration(), - brokenSitePromptLimiter: BrokenSitePromptLimiter = .shared) { + brokenSitePromptLimiter: BrokenSitePromptLimiter = .shared, + featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger + ) { self.aiChatMenuConfig = aiChatMenuConfig let tabCollectionViewModel = tabCollectionViewModel ?? TabCollectionViewModel() self.tabCollectionViewModel = tabCollectionViewModel self.isBurner = tabCollectionViewModel.isBurner + self.featureFlagger = featureFlagger tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) bookmarksBarVisibilityManager = BookmarksBarVisibilityManager(selectedTabPublisher: tabCollectionViewModel.$selectedTabViewModel.eraseToAnyPublisher()) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 00c118a05b..aaeb844a67 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -21,6 +21,7 @@ import Cocoa import Common import Configuration import Crashes +import FeatureFlags import History import PixelKit import Subscription @@ -1018,7 +1019,9 @@ extension MainViewController { // MARK: - Developer Tools @objc func toggleDeveloperTools(_ sender: Any?) { - guard let webView = getActiveTabAndIndex()?.tab.webView else { return } + guard let webView = browserTabViewController.webView else { + return + } if webView.isInspectorShown == true { webView.closeDeveloperTools() @@ -1028,15 +1031,15 @@ extension MainViewController { } @objc func openJavaScriptConsole(_ sender: Any?) { - getActiveTabAndIndex()?.tab.webView.openJavaScriptConsole() + browserTabViewController.webView?.openJavaScriptConsole() } @objc func showPageSource(_ sender: Any?) { - getActiveTabAndIndex()?.tab.webView.showPageSource() + browserTabViewController.webView?.showPageSource() } @objc func showPageResources(_ sender: Any?) { - getActiveTabAndIndex()?.tab.webView.showPageSource() + browserTabViewController.webView?.showPageSource() } } @@ -1133,7 +1136,7 @@ extension MainViewController: NSMenuItemValidation { case #selector(MainViewController.openJavaScriptConsole(_:)), #selector(MainViewController.showPageSource(_:)), #selector(MainViewController.showPageResources(_:)): - return activeTabViewModel?.canReload == true || (activeTabViewModel?.tab.url?.isNewTabPage == true && NSApp.delegateTyped.featureFlagger.isFeatureOn(.htmlNewTabPage)) + return activeTabViewModel?.canReload == true || (activeTabViewModel?.tab.url?.isNewTabPage == true && featureFlagger.isFeatureOn(.htmlNewTabPage)) case #selector(MainViewController.toggleDownloads(_:)): let isDownloadsPopoverShown = self.navigationBarViewController.isDownloadsPopoverShown diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 0145d02131..f39a02383c 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -42,7 +42,7 @@ final class BrowserTabViewController: NSViewController { private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) private let newTabPageUserScript: NewTabPageUserScript - private lazy var newTabPageWebView: WebView = { + private(set) lazy var newTabPageWebView: WebView = { let configuration = WKWebViewConfiguration() configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger) @@ -53,7 +53,7 @@ final class BrowserTabViewController: NSViewController { return webView }() - private weak var webView: WebView? + private(set) weak var webView: WebView? private weak var webViewContainer: NSView? private weak var webViewSnapshot: NSView? private var containerStackView: NSStackView @@ -537,7 +537,6 @@ final class BrowserTabViewController: NSViewController { webView = newWebView addWebViewToViewHierarchy(newWebView, tab: tabViewModel.tab) - } guard let tabViewModel = tabViewModel else { From c1dc66e4eae1a69565b03faa5d9ac494e245a05d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 10:37:24 +0100 Subject: [PATCH 32/42] Clean up UserDefaultsWrapper --- .../Utilities/UserDefaultsWrapper.swift | 4 - .../Utilities/UserDefaultsWrapper.swift.orig | 416 ------------------ 2 files changed, 420 deletions(-) delete mode 100644 DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index f5700b76c6..9939f5b3df 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -232,10 +232,6 @@ public struct UserDefaultsWrapper { case lastBrokenSiteToastShownDate = "brokenSitePrompt.last-broken-site-toast-shown-date" case toastDismissStreakCounter = "brokenSitePrompt.toast-dismiss-streak-counter" - - // Experimental Feature Flags - - case htmlNewTabPage = "experimental.html-new-tab-page" } enum RemovedKeys: String, CaseIterable { diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig deleted file mode 100644 index a8c2d4f76e..0000000000 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift.orig +++ /dev/null @@ -1,416 +0,0 @@ -// -// UserDefaultsWrapper.swift -// -// Copyright © 2021 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 AppKit -import Foundation -import AppKitExtensions - -extension UserDefaults { - /// The app group's shared UserDefaults - static let netP = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .netP))! - static let dbp = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .dbp))! - static let subs = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .subs))! - static let appConfiguration = UserDefaults(suiteName: Bundle.main.appGroup(bundle: .appConfiguration))! -} - -public struct UserDefaultsWrapperKey: RawRepresentable { - public let rawValue: String - public init(rawValue: String) { - self.rawValue = rawValue - } -} - -@propertyWrapper -public struct UserDefaultsWrapper { - - public typealias DefaultsKey = UserDefaultsWrapperKey - - public enum Key: String, CaseIterable { - /// system setting defining window title double-click action - case appleActionOnDoubleClick = "AppleActionOnDoubleClick" - - case fireproofDomains = "com.duckduckgo.fireproofing.allowedDomains" - case areDomainsMigratedToETLDPlus1 = "com.duckduckgo.are-domains-migrated-to-etldplus1" - case unprotectedDomains = "com.duckduckgo.contentblocker.unprotectedDomains" - case contentBlockingRulesCache = "com.duckduckgo.contentblocker.rules.cache" - - case defaultBrowserDismissed = "browser.default.dismissed" - - case spellingCheckEnabledOnce = "spelling.check.enabled.once" - case grammarCheckEnabledOnce = "grammar.check.enabled.once" - - case loginDetectionEnabled = "fireproofing.login-detection-enabled" - case autoClearEnabled = "preferences.auto-clear-enabled" - case warnBeforeClearingEnabled = "preferences.warn-before-clearing-enabled" - case gpcEnabled = "preferences.gpc-enabled" - case selectedDownloadLocationKey = "preferences.download-location" - case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location" - case alwaysRequestDownloadLocationKey = "preferences.download-location.always-request" - case openDownloadsPopupOnCompletionKey = "preferences.downloads.open.on.completion" - case autoconsentEnabled = "preferences.autoconsent-enabled" - case autoconsentFilterlistExperimentCohort = "preferences.autoconsent.filterListExperimentCohort" - case duckPlayerMode = "preferences.duck-player" - case youtubeOverlayInteracted = "preferences.youtube-overlay-interacted" - case youtubeOverlayButtonsUsed = "preferences.youtube-overlay-user-used-buttons" - case duckPlayerAutoplay = "preferences.duckplayer.autoplay" - case duckPlayerOpenInNewTab = "preferences.duckplayer.open-new-tab" - - case selectedPasswordManager = "preferences.autofill.selected-password-manager" - - case askToSaveUsernamesAndPasswords = "preferences.ask-to-save.usernames-passwords" - case askToSaveAddresses = "preferences.ask-to-save.addresses" - case askToSavePaymentMethods = "preferences.ask-to-save.payment-methods" - case autolockLocksFormFilling = "preferences.lock-autofill-form-fill" - case autofillDebugScriptEnabled = "preferences.enable-autofill-debug-script" - - case saveAsPreferredFileType = "saveAs.selected.filetype" - - case lastCrashReportCheckDate = "last.crash.report.check.date" - case didCrashDuringCrashHandlersSetUp = "browser.didCrashDuringCrashHandlersSetUp" - - case fireInfoPresentedOnce = "fire.info.presented.once" - case appTerminationHandledCorrectly = "app.termination.handled.correctly" - case restoreTabsOnStartup = "restore.tabs.on.startup" - - case restorePreviousSession = "preferences.startup.restore-previous-session" - case launchToCustomHomePage = "preferences.startup.launch-to-custom-home-page" - case customHomePageURL = "preferences.startup.customHomePageURL" - case currentThemeName = "com.duckduckgo.macos.currentThemeNameKey" - case showFullURL = "preferences.appearance.show-full-url" - case showAutocompleteSuggestions = "preferences.appearance.show-autocomplete-suggestions" - case preferNewTabsToWindows = "preferences.tabs.prefer-new-tabs-to-windows" - case switchToNewTabWhenOpened = "preferences.tabs.switch-to-new-tab-when-opened" - case newTabPosition = "preferences.tabs.new-tab-position" - case defaultPageZoom = "preferences.appearance.default-page-zoom" - case websitePageZoom = "preferences.appearance.website-page-zoom" - case bookmarksBarAppearance = "preferences.appearance.bookmarks-bar" - - case homeButtonPosition = "preferences.appeareance.home-button-position" - - case phishingDetectionEnabled = "preferences.security.phishing-detection-enabled" - - // ATB - case installDate = "statistics.installdate.key" - case atb = "statistics.atb.key" - case searchRetentionAtb = "statistics.retentionatb.key" - case appRetentionAtb = "statistics.appretentionatb.key" - case lastAppRetentionRequestDate = "statistics.appretentionatb.last.request.key" - - // Used to detect whether a user had old User Defaults ATB data at launch, in order to grant them implicitly - // unlocked status with regards to the lock screen - case legacyStatisticsStoreDataCleared = "statistics.appretentionatb.legacy-data-cleared" - - case onboardingFinished = "onboarding.finished" - case contextualOnboardingState = "contextual.onboarding.state" - - // Home Page - case homePageShowPagesOnHover = "home.page.show.pages.on.hover" - case homePageShowAllFavorites = "home.page.show.all.favorites" - case homePageShowAllFeatures = "home.page.show.all.features" - case homePageShowMakeDefault = "home.page.show.make.default" - case homePageShowAddToDock = "home.page.show.add.to.dock" - case homePageShowImport = "home.page.show.import" - case homePageShowDuckPlayer = "home.page.show.duck.player" - case homePageShowEmailProtection = "home.page.show.email.protection" - case homePageShowPageTitles = "home.page.show.page.titles" - case homePageShowRecentlyVisited = "home.page.show.recently.visited" - case homePageContinueSetUpImport = "home.page.continue.set.up.import" - case homePageIsFavoriteVisible = "home.page.is.favorite.visible" - case homePageIsContinueSetupVisible = "home.page.is.continue.setup.visible" - case homePageIsRecentActivityVisible = "home.page.is.recent.activity.visible" - case homePageIsSearchBarVisible = "home.page.is.search.bar.visible" - case homePageIsFirstSession = "home.page.is.first.session" - case homePageDidShowSettingsOnboarding = "home.page.did.show.settings.onboarding" - case homePageUserBackgroundImages = "home.page.user.background.images" - case homePageCustomBackground = "home.page.custom.background" - case homePageLastPickedCustomColor = "home.page.last.picked.custom.color" - - case homePagePromotionVisible = "home.page.promotion.visible" - case homePagePromotionDidDismiss = "home.page.promotion.did.dismiss" - - case appIsRelaunchingAutomatically = "app-relaunching-automatically" - - case historyV5toV6Migration = "history.v5.to.v6.migration.2" - case emailKeychainMigration = "email.keychain.migration" - - case bookmarksBarPromptShown = "bookmarks.bar.prompt.shown" - case showBookmarksBar = "bookmarks.bar.show" - case centerAlignedBookmarksBar = "bookmarks.bar.center.aligned" - case lastBookmarksBarUsagePixelSendDate = "bookmarks.bar.last-usage-pixel-send-date" - - case pinnedViews = "pinning.pinned-views" - case manuallyToggledPinnedViews = "pinning.manually-toggled-pinned-views" - - case lastDatabaseFactoryFailurePixelDate = "last.database.factory.failure.pixel.date" - - case loggingEnabledDate = "logging.enabled.date" - case loggingCategories = "logging.categories" - - case firstLaunchDate = "first.app.launch.date" - case customConfigurationUrl = "custom.configuration.url" - - case lastRemoteMessagingRefreshDate = "last.remote.messaging.refresh.date" - - // Data Broker Protection - - case dataBrokerProtectionTermsAndConditionsAccepted = "data-broker-protection.waitlist-terms-and-conditions.accepted" - case shouldShowDBPWaitlistInvitedCardUI = "shouldShowDBPWaitlistInvitedCardUI" - - // VPN - - case networkProtectionExcludedRoutes = "netp.excluded-routes" - - // VPN: Shared Defaults - // --- - // Please note that shared defaults MUST have a name that matches exactly their value, - // or else KVO will just not work as of 2023-08-07 - - case networkProtectionOnboardingStatusRawValue = "networkProtectionOnboardingStatusRawValue" - - // Updates - case automaticUpdates = "updates.automatic" - case pendingUpdateShown = "pending.update.shown" - - // Experiments - case pixelExperimentInstalled = "pixel.experiment.installed" - case pixelExperimentCohort = "pixel.experiment.cohort" - case pixelExperimentEnrollmentDate = "pixel.experiment.enrollment.date" - case pixelExperimentFiredPixels = "pixel.experiment.pixels.fired" - case campaignVariant = "campaign.variant" - - // Updates - case previousAppVersion = "previous.app.version" - case previousBuild = "previous.build" - - // Sync - - case syncEnvironment = "sync.environment" - case favoritesDisplayMode = "sync.favorites-display-mode" - case syncBookmarksPaused = "sync.bookmarks-paused" - case syncCredentialsPaused = "sync.credentials-paused" - case syncIsPaused = "sync.paused" - case syncBookmarksPausedErrorDisplayed = "sync.bookmarks-paused-error-displayed" - case syncCredentialsPausedErrorDisplayed = "sync.credentials-paused-error-displayed" - case syncInvalidLoginPausedErrorDisplayed = "sync.invalid-login-paused-error-displayed" - case syncIsFaviconsFetcherEnabled = "sync.is-favicons-fetcher-enabled" - case syncIsEligibleForFaviconsFetcherOnboarding = "sync.is-eligible-for-favicons-fetcher-onboarding" - case syncDidPresentFaviconsFetcherOnboarding = "sync.did-present-favicons-fetcher-onboarding" - case syncDidMigrateToImprovedListsHandling = "sync.did-migrate-to-improved-lists-handling" - case syncDidShowSyncPausedByFeatureFlagAlert = "sync.did-show-sync-paused-by-feature-flag-alert" - case syncLastErrorNotificationTime = "sync.last-error-notification-time" - case syncLastSuccesfullTime = "sync.last-time-success" - case syncLastNonActionableErrorCount = "sync.non-actionable-error-count" - case syncCurrentAllPausedError = "sync.current-all-paused-error" - case syncCurrentBookmarksPausedError = "sync.current-bookmarks-paused-error" - case syncCurrentCredentialsPausedError = "sync.current-credentials-paused-error" - case syncPromoBookmarksDismissed = "sync.promotion-bookmarks-dismissed" - case syncPromoPasswordsDismissed = "sync.promotion-passwords-dismissed" - - // Subscription - - case subscriptionEnvironment = "subscription.environment" - -<<<<<<< HEAD - // Experimental Feature Flags - - case htmlNewTabPage = "experimental.html-new-tab-page" -======= - // PageRefreshMonitor - - case refreshTimestamps = "pageRefreshMonitor.refresh-timestamps" - - // BrokenSitePrompt - - case lastBrokenSiteToastShownDate = "brokenSitePrompt.last-broken-site-toast-shown-date" - case toastDismissStreakCounter = "brokenSitePrompt.toast-dismiss-streak-counter" - ->>>>>>> main - } - - enum RemovedKeys: String, CaseIterable { - case passwordManagerDoNotPromptDomains = "com.duckduckgo.passwordmanager.do-not-prompt-domains" - case incrementalFeatureFlagTestHasSentPixel = "network-protection.incremental-feature-flag-test.has-sent-pixel" - case homePageShowNetworkProtectionBetaEndedNotice = "home.page.network-protection.show-beta-ended-notice" - - // NetP removed keys - case networkProtectionShouldEnforceRoutes = "netp.enforce-routes" - case networkProtectionShouldIncludeAllNetworks = "netp.include-all-networks" - case networkProtectionConnectionTesterEnabled = "netp.connection-tester-enabled" - case networkProtectionShouldExcludeLocalNetworks = "netp.exclude-local-routes" - case networkProtectionRegistrationKeyValidity = "com.duckduckgo.network-protection.NetworkProtectionTunnelController.registrationKeyValidityKey" - case shouldShowNetworkProtectionSystemExtensionUpgradePrompt = "network-protection.show-system-extension-upgrade-prompt" - } - - private let key: DefaultsKey - private let getter: (Any?) -> T - private let setter: (UserDefaultsWrapper, T) -> Void - - private let customUserDefaults: UserDefaults? - - var defaults: UserDefaults { - customUserDefaults ?? Self.sharedDefaults - } - - static var sharedDefaults: UserDefaults { -#if DEBUG && !(NETP_SYSTEM_EXTENSION && NETWORK_EXTENSION) // Avoid looking up special user defaults when running inside the system extension - if case .normal = NSApplication.runType { - return .standard - } else { - return UserDefaults(suiteName: "\(Bundle.main.bundleIdentifier!).\(NSApplication.runType)")! - } -#else - return .standard -#endif - } - - public init(key: DefaultsKey, defaults: UserDefaults? = nil, getter: @escaping (Any?) -> T, setter: @escaping (UserDefaultsWrapper, T) -> Void) { - self.key = key - self.getter = getter - self.setter = setter - self.customUserDefaults = defaults - } - - @_disfavoredOverload - public init(key: DefaultsKey, defaultValue: T, defaults: UserDefaults? = nil) { - @inline(__always) func isNil(_ value: T) -> Bool { - @inline(__always) func cast(_ value: V, to _: R.Type) -> R? { - value as? R // perform `value as? Any?` suppressing `casting to Any? always succeeds` warning - } - return if case .some(.none) = cast(value, to: Any?.self) { true } else { false } - } - - self.init(key: key, defaults: defaults) { - guard let value = $0 as? T, !isNil(value) else { return defaultValue } - return value - } setter: { this, newValue in - guard PropertyListSerialization.propertyList(newValue, isValidFor: .binary) else { - if isNil(newValue) { - this.defaults.removeObject(forKey: key.rawValue) - return - } - assertionFailure("\(newValue) cannot be stored in UserDefaults") - return - } - this.defaults.set(newValue, forKey: key.rawValue) - } - } - - public init(key: DefaultsKey, defaults: UserDefaults? = nil) where T == Wrapped? { - self.init(key: key, defaults: defaults) { - $0 as? Wrapped - } setter: { this, newValue in - guard let newValue else { - this.defaults.removeObject(forKey: key.rawValue) // newValue is nil - return - } - guard PropertyListSerialization.propertyList(newValue, isValidFor: .binary) else { - assertionFailure("\(newValue) cannot be stored in UserDefaults") - return - } - this.defaults.set(newValue, forKey: key.rawValue) - } - } - - @available(*, unavailable, message: "Cannot use overload with `defaultValue` for an Optional Value") - public init(key: DefaultsKey, defaultValue: Wrapped, defaults: UserDefaults? = nil) where T == Wrapped? { - fatalError() - } - - public init(key: DefaultsKey, defaultValue: T, defaults: UserDefaults? = nil) where T: RawRepresentable { - self.init(key: key, defaults: defaults) { - ($0 as? RawValue).flatMap(T.init(rawValue:)) ?? defaultValue - } setter: { this, newValue in - guard PropertyListSerialization.propertyList(newValue.rawValue, isValidFor: .binary) else { - assertionFailure("\(newValue.rawValue) cannot be stored in UserDefaults") - return - } - this.defaults.set(newValue.rawValue, forKey: key.rawValue) - } - } - - public init(key: DefaultsKey, defaults: UserDefaults? = nil) where T == Wrapped?, Wrapped: RawRepresentable { - self.init(key: key, defaults: defaults) { - ($0 as? RawValue).flatMap(Wrapped.init(rawValue:)) - } setter: { this, newValue in - guard let newValue else { - this.defaults.removeObject(forKey: key.rawValue) // newValue is nil - return - } - guard PropertyListSerialization.propertyList(newValue.rawValue, isValidFor: .binary) else { - assertionFailure("\(newValue.rawValue) cannot be stored in UserDefaults") - return - } - this.defaults.set(newValue.rawValue, forKey: key.rawValue) - } - } - - @_disfavoredOverload - public init(key: Key, defaultValue: T, defaults: UserDefaults? = nil) { - self.init(key: .init(rawValue: key.rawValue), defaultValue: defaultValue, defaults: defaults) - } - - public init(key: Key, defaults: UserDefaults? = nil) where T == Wrapped? { - self.init(key: .init(rawValue: key.rawValue), defaults: defaults) - } - - public init(key: Key, defaultValue: T, defaults: UserDefaults? = nil) where T: RawRepresentable { - self.init(key: .init(rawValue: key.rawValue), defaultValue: defaultValue, defaults: defaults) - } - - public init(key: Key, defaults: UserDefaults? = nil) where T == Wrapped?, Wrapped: RawRepresentable { - self.init(key: .init(rawValue: key.rawValue), defaults: defaults) - } - - public var wrappedValue: T { - get { - let storedValue = defaults.object(forKey: key.rawValue) - return getter(storedValue) - } - nonmutating set { - setter(self, newValue) - } - } - - static func clearAll() { - let defaults = sharedDefaults - Key.allCases.forEach { key in - defaults.removeObject(forKey: key.rawValue) - } - } - - static func clearRemovedKeys() { - let defaults = sharedDefaults - RemovedKeys.allCases.forEach { key in - defaults.removeObject(forKey: key.rawValue) - } - } - - func clear() { - defaults.removeObject(forKey: key.rawValue) - } - -} - -extension UserDefaultsWrapper where T == Any { - static func clear(_ key: Key) { - sharedDefaults.removeObject(forKey: key.rawValue) - } - static func clear(_ key: DefaultsKey) { - sharedDefaults.removeObject(forKey: key.rawValue) - } -} From 530278f498ac647ca89a5e35d66ed49e0366211a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 10:38:25 +0100 Subject: [PATCH 33/42] Add Asana project link to htmlNewTabPage flag --- .../FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift index 59725f11e5..933dcb23e3 100644 --- a/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift +++ b/LocalPackages/FeatureFlags/Sources/FeatureFlags/FeatureFlag.swift @@ -45,6 +45,7 @@ public enum FeatureFlag: String, CaseIterable { /// https://app.asana.com/0/72649045549333/1208617860225199/f case networkProtectionEnforceRoutes + /// https://app.asana.com/0/72649045549333/1208241266421040/f case htmlNewTabPage } From 76a6a1f452c7c4280984f533d33422087738c111 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 10:41:13 +0100 Subject: [PATCH 34/42] Remove unused experimentalFeaturesMenu --- DuckDuckGo/Menus/MainMenu.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 87f5eee377..eafcb07d1b 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -92,7 +92,6 @@ final class MainMenu: NSMenu { let windowsMenu = NSMenu(title: UserText.mainMenuWindow) // MARK: Debug - private var experimentalFeaturesMenu: NSMenu? private var loggingMenu: NSMenu? let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil) From 68916f04b5d648cd165263d057d0157e11f13d52 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 13:54:04 +0100 Subject: [PATCH 35/42] Clean up the code --- DuckDuckGo/Tab/UserScripts/UserScripts.swift | 1 - DuckDuckGo/Tab/View/BrowserTabViewController.swift | 9 ++------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/Tab/UserScripts/UserScripts.swift b/DuckDuckGo/Tab/UserScripts/UserScripts.swift index 65936c4e8c..a0444dfef0 100644 --- a/DuckDuckGo/Tab/UserScripts/UserScripts.swift +++ b/DuckDuckGo/Tab/UserScripts/UserScripts.swift @@ -120,7 +120,6 @@ final class UserScripts: UserScriptsProvider { if let onboardingUserScript { specialPages.registerSubfeature(delegate: onboardingUserScript) } - userScripts.append(specialPages) } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 1e61a35e23..173b5fc67a 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -553,7 +553,7 @@ final class BrowserTabViewController: NSViewController { } func displayWebView(of tabViewModel: TabViewModel) { - let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTabPage == true ? newTabPageWebView : tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content == .newtab ? newTabPageWebView : tabViewModel.tab.webView cleanUpRemoteWebViewIfNeeded(newWebView) webView = newWebView @@ -830,11 +830,6 @@ final class BrowserTabViewController: NSViewController { } } - func refreshTab() { - guard let tabViewModel else { return } - showTabContent(of: tabViewModel) - } - func updateTabIfNeeded(tabViewModel: TabViewModel?) { if shouldReplaceWebView(for: tabViewModel) { removeAllTabContent(includingWebView: true) @@ -860,7 +855,7 @@ final class BrowserTabViewController: NSViewController { return false } - let newWebView = tabViewModel.tab.content.urlForWebView?.isNewTabPage == true ? newTabPageWebView : tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content == .newtab ? newTabPageWebView : tabViewModel.tab.webView let isPinnedTab = tabCollectionViewModel.pinnedTabsCollection?.tabs.contains(tabViewModel.tab) == true let isKeyWindow = view.window?.isKeyWindow == true From 5c7b72c93794b124b27aedd929488223edac5cf8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 13:54:10 +0100 Subject: [PATCH 36/42] Add placeholder Privacy Stats view --- .../HomePage/NewTabPageActionsManager.swift | 18 +++++++++-- .../HomePage/NewTabPageUserScript.swift | 32 ++++++++++++++++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index 50650c258b..4c2c01cb50 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -27,7 +27,11 @@ protocol NewTabPageActionsManaging: AnyObject { var userScript: NewTabPageUserScript? { get set } func getFavorites() -> NewTabPageUserScript.FavoritesData - func getFavoritesConfig() -> NewTabPageUserScript.FavoritesConfig + func getFavoritesConfig() -> NewTabPageUserScript.WidgetConfig + + func getPrivacyStats() -> NewTabPageUserScript.PrivacyStatsData + func getPrivacyStatsConfig() -> NewTabPageUserScript.WidgetConfig + /// It is called in case of error loading the pages func reportException(with params: [String: String]) func showContextMenu(with params: [String: Any]) @@ -127,7 +131,17 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { .init(favorites: []) } - func getFavoritesConfig() -> NewTabPageUserScript.FavoritesConfig { + func getFavoritesConfig() -> NewTabPageUserScript.WidgetConfig { + // implementation TBD + .init(animation: .auto, expansion: .collapsed) + } + + func getPrivacyStats() -> NewTabPageUserScript.PrivacyStatsData { + // implementation TBD + .init(totalCount: 0, trackerCompanies: []) + } + + func getPrivacyStatsConfig() -> NewTabPageUserScript.WidgetConfig { // implementation TBD .init(animation: .auto, expansion: .collapsed) } diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index c6beec16a3..6c6fc1a0bf 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -36,6 +36,8 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { case initialSetup case reportInitException case reportPageException + case statsGetConfig = "stats_getConfig" + case statsGetData = "stats_getData" case widgetsSetConfig = "widgets_setConfig" } @@ -56,6 +58,8 @@ final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { .initialSetup: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, .reportInitException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, .reportPageException: { [weak self] in try await self?.reportException(params: $0, original: $1) }, + .statsGetConfig: { [weak self] in try await self?.statsGetConfig(params: $0, original: $1) }, + .statsGetData: { [weak self] in try await self?.statsGetData(params: $0, original: $1) }, .widgetsSetConfig: { [weak self] in try await self?.widgetsSetConfig(params: $0, original: $1) } ] @@ -95,6 +99,16 @@ extension NewTabPageUserScript { actionsManager.configuration } + @MainActor + private func statsGetConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + actionsManager.getPrivacyStatsConfig() + } + + @MainActor + private func statsGetData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + actionsManager.getPrivacyStats() + } + @MainActor private func widgetsSetConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [[String: String]] else { return nil } @@ -111,7 +125,7 @@ extension NewTabPageUserScript { extension NewTabPageUserScript { - struct FavoritesConfig: Encodable { + struct WidgetConfig: Encodable { let animation: Animation? let expansion: Expansion } @@ -135,7 +149,7 @@ extension NewTabPageUserScript { } struct FavoritesData: Encodable { - var favorites: [Favorite] + let favorites: [Favorite] } struct Favorite: Encodable { @@ -146,7 +160,17 @@ extension NewTabPageUserScript { } struct FavoriteFavicon: Encodable { - var maxAvailableSize: Int - var src: String + let maxAvailableSize: Int + let src: String + } + + struct PrivacyStatsData: Encodable { + let totalCount: Int + let trackerCompanies: [TrackerCompany] + } + + struct TrackerCompany: Encodable { + let count: Int + let displayName: String } } From 5eee236656c2e6a8276b97a276b7fa5f820f855d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 13:57:31 +0100 Subject: [PATCH 37/42] Update MainMenuActions --- DuckDuckGo/Menus/MainMenuActions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index aaeb844a67..c563f3db8e 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -1136,7 +1136,7 @@ extension MainViewController: NSMenuItemValidation { case #selector(MainViewController.openJavaScriptConsole(_:)), #selector(MainViewController.showPageSource(_:)), #selector(MainViewController.showPageResources(_:)): - return activeTabViewModel?.canReload == true || (activeTabViewModel?.tab.url?.isNewTabPage == true && featureFlagger.isFeatureOn(.htmlNewTabPage)) + return activeTabViewModel?.canReload == true || (featureFlagger.isFeatureOn(.htmlNewTabPage) && activeTabViewModel?.tab.content == .newtab) case #selector(MainViewController.toggleDownloads(_:)): let isDownloadsPopoverShown = self.navigationBarViewController.isDownloadsPopoverShown From 98c252cf244f548aca5c9823da1e8ccc1d5e195d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 15:22:17 +0100 Subject: [PATCH 38/42] Add NewTabPageWebView --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +++ DuckDuckGo/HomePage/NewTabPageWebView.swift | 42 +++++++++++++++++++ .../Tab/View/BrowserTabViewController.swift | 8 +--- DuckDuckGo/Tab/View/WebView.swift | 22 +++++----- 4 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 DuckDuckGo/HomePage/NewTabPageWebView.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d92c912e4c..561890e700 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1135,6 +1135,8 @@ 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA2281132CB002621E7 /* TabLazyLoaderDataSource.swift */; }; 37534CA52811987D002621E7 /* AdjacentItemEnumeratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA42811987D002621E7 /* AdjacentItemEnumeratorTests.swift */; }; 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */; }; + 3758CBAB2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */; }; + 3758CBAC2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */; }; 376113CC2B29CD5B00E794BB /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; 376705AF27EB488600DD8D76 /* RoundedSelectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */; }; 376731822C7E226A00EB097B /* HomePageViewBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731812C7E226A00EB097B /* HomePageViewBackground.swift */; }; @@ -3614,6 +3616,7 @@ 37534CA2281132CB002621E7 /* TabLazyLoaderDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLazyLoaderDataSource.swift; sourceTree = ""; }; 37534CA42811987D002621E7 /* AdjacentItemEnumeratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjacentItemEnumeratorTests.swift; sourceTree = ""; }; 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjacentItemEnumerator.swift; sourceTree = ""; }; + 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageWebView.swift; sourceTree = ""; }; 376113C52B29BCD600E794BB /* SyncE2EUITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITests.xcconfig; sourceTree = ""; }; 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SyncE2EUITests App Store.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 376113D72B29D0F800E794BB /* SyncE2EUITestsAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITestsAppStore.xcconfig; sourceTree = ""; }; @@ -8761,6 +8764,7 @@ 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */, 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */, 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */, + 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */, 85589E8527BBB8DD0038AD11 /* Model */, AAE71DB325F66A3F00D74437 /* View */, 85AC7ADA27BD628400FFB69B /* HomePage.swift */, @@ -11652,6 +11656,7 @@ 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 37D0469E2C7D0EDD00AEAA50 /* CustomBackground.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, + 3758CBAB2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */, 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, @@ -13073,6 +13078,7 @@ AA68C3D32490ED62001B8783 /* NavigationBarViewController.swift in Sources */, AA585DAF2490E6E600E9A3E2 /* MainViewController.swift in Sources */, F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, + 3758CBAC2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */, 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */, AA5FA69A275F91C700DCE9C9 /* Favicon.swift in Sources */, AABEE69A24A902A90043105B /* SuggestionContainerViewModel.swift in Sources */, diff --git a/DuckDuckGo/HomePage/NewTabPageWebView.swift b/DuckDuckGo/HomePage/NewTabPageWebView.swift new file mode 100644 index 0000000000..171d989a4d --- /dev/null +++ b/DuckDuckGo/HomePage/NewTabPageWebView.swift @@ -0,0 +1,42 @@ +// +// NewTabPageWebView.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 BrowserServicesKit +import WebKit + +final class NewTabPageWebView: WebView { + + init(featureFlagger: FeatureFlagger) { + let configuration = WKWebViewConfiguration() + configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger) + + super.init(frame: .zero, configuration: configuration) + navigationDelegate = self + load(URLRequest(url: URL.newtab)) + } + + @MainActor required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension NewTabPageWebView: WKNavigationDelegate { + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + navigationAction.request.url == .newtab ? .allow : .cancel + } +} diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 173b5fc67a..82ca8775f8 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -44,14 +44,8 @@ final class BrowserTabViewController: NSViewController { private let newTabPageUserScript: NewTabPageUserScript private(set) lazy var newTabPageWebView: WebView = { - let configuration = WKWebViewConfiguration() - configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger) - - let webView = WebView(frame: .zero, configuration: configuration) - + let webView = NewTabPageWebView(featureFlagger: featureFlagger) newTabPageUserScript.webViews.add(webView) - webView.load(URLRequest(url: URL.newtab)) - return webView }() private(set) weak var webView: WebView? diff --git a/DuckDuckGo/Tab/View/WebView.swift b/DuckDuckGo/Tab/View/WebView.swift index f421f32037..83c7a65910 100644 --- a/DuckDuckGo/Tab/View/WebView.swift +++ b/DuckDuckGo/Tab/View/WebView.swift @@ -39,7 +39,7 @@ protocol WebViewZoomLevelDelegate: AnyObject { } @objc(DuckDuckGo_WebView) -final class WebView: WKWebView { +open class WebView: WKWebView { weak var contextMenuDelegate: WebViewContextMenuDelegate? weak var interactionEventsDelegate: WebViewInteractionEventsDelegate? @@ -53,7 +53,7 @@ final class WebView: WKWebView { isInspectorShown && window != nil } - override func addTrackingArea(_ trackingArea: NSTrackingArea) { + open override func addTrackingArea(_ trackingArea: NSTrackingArea) { /// disable mouseEntered/mouseMoved/mouseExited events passing to Web View while it‘s loading /// see https://app.asana.com/0/1177771139624306/1206990108527681/f if trackingArea.owner?.className == "WKMouseTrackingObserver" { @@ -75,7 +75,7 @@ final class WebView: WKWebView { super.addTrackingArea(trackingArea) } - override var isInFullScreenMode: Bool { + open override var isInFullScreenMode: Bool { if #available(macOS 13.0, *) { return self.fullscreenState != .notInFullscreen } else { @@ -160,29 +160,29 @@ final class WebView: WKWebView { // MARK: - Menu - override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + open override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { super.willOpenMenu(menu, with: event) contextMenuDelegate?.webView(self, willOpenContextMenu: menu, with: event) } - override func didCloseMenu(_ menu: NSMenu, with event: NSEvent?) { + open override func didCloseMenu(_ menu: NSMenu, with event: NSEvent?) { super.didCloseMenu(menu, with: event) contextMenuDelegate?.webView(self, didCloseContextMenu: menu, with: event) } // MARK: - Events - override func mouseDown(with event: NSEvent) { + open override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) interactionEventsDelegate?.webView(self, mouseDown: event) } - override func keyDown(with event: NSEvent) { + open override func keyDown(with event: NSEvent) { super.keyDown(with: event) interactionEventsDelegate?.webView(self, keyDown: event) } - override func scrollWheel(with event: NSEvent) { + open override func scrollWheel(with event: NSEvent) { super.scrollWheel(with: event) interactionEventsDelegate?.webView(self, scrollWheel: event) } @@ -198,7 +198,7 @@ final class WebView: WKWebView { } } - override func viewDidMoveToWindow() { + open override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if shouldShowWebInspector { openDeveloperTools() @@ -268,7 +268,7 @@ final class WebView: WKWebView { // MARK: - NSDraggingDestination - override func draggingUpdated(_ draggingInfo: NSDraggingInfo) -> NSDragOperation { + open override func draggingUpdated(_ draggingInfo: NSDraggingInfo) -> NSDragOperation { if draggingInfo.draggingSource is WebView { return super.draggingUpdated(draggingInfo) } @@ -285,7 +285,7 @@ final class WebView: WKWebView { return superview.draggingUpdated(draggingInfo) } - override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { + open override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { if draggingInfo.draggingSource is WebView { return super.performDragOperation(draggingInfo) } From c164e4153053acf9f7006788f58afb65927bbceb Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 15:55:06 +0100 Subject: [PATCH 39/42] Remove @preconcurrency until we're on Xcode 16 --- DuckDuckGo/HomePage/NewTabPageUserScript.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index 6c6fc1a0bf..45da589213 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -20,7 +20,7 @@ import Foundation import UserScript import WebKit -final class NewTabPageUserScript: NSObject, @preconcurrency Subfeature { +final class NewTabPageUserScript: NSObject, Subfeature { let actionsManager: NewTabPageActionsManaging var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "newtab")]) From 80b86419fe44a3058aa84160ceead5ad4074f05d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 14 Nov 2024 16:41:32 +0100 Subject: [PATCH 40/42] Use 1 User Script per Web View --- DuckDuckGo/Application/AppDelegate.swift | 2 - .../HomePage/NewTabPageActionsManager.swift | 60 +++++-------------- .../NewTabPageUserContentController.swift | 12 ++-- .../HomePage/NewTabPageUserScript.swift | 44 ++++++++++++-- DuckDuckGo/HomePage/NewTabPageWebView.swift | 7 ++- .../SpecialPagesUserScriptExtension.swift | 5 -- .../Tab/View/BrowserTabViewController.swift | 13 ++-- 7 files changed, 71 insertions(+), 72 deletions(-) diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 1b25165c14..460932024b 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -93,7 +93,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { var privacyDashboardWindow: NSWindow? let newTabPageActionsManager: NewTabPageActionsManaging - let newTabPageUserScript: NewTabPageUserScript let activeRemoteMessageModel: ActiveRemoteMessageModel let homePageSettingsModel = HomePage.Models.SettingsModel() let remoteMessagingClient: RemoteMessagingClient! @@ -312,7 +311,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { freemiumDBPFeature: freemiumDBPFeature) newTabPageActionsManager = NewTabPageActionsManager(appearancePreferences: .shared) - newTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) } func applicationWillFinishLaunching(_ notification: Notification) { diff --git a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift index 4c2c01cb50..2325e2cf85 100644 --- a/DuckDuckGo/HomePage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/HomePage/NewTabPageActionsManager.swift @@ -23,8 +23,9 @@ import Common import os.log protocol NewTabPageActionsManaging: AnyObject { - var configuration: NewTabPageConfiguration { get } - var userScript: NewTabPageUserScript? { get set } + var configuration: NewTabPageUserScript.NewTabPageConfiguration { get } + + func registerUserScript(_ userScript: NewTabPageUserScript) func getFavorites() -> NewTabPageUserScript.FavoritesData func getFavoritesConfig() -> NewTabPageUserScript.WidgetConfig @@ -38,46 +39,11 @@ protocol NewTabPageActionsManaging: AnyObject { func updateWidgetConfigs(with params: [[String: String]]) } -struct NewTabPageConfiguration: Encodable { - var widgets: [Widget] - var widgetConfigs: [WidgetConfig] - var env: String - var locale: String - var platform: Platform - - struct Widget: Encodable { - var id: String - } - - struct WidgetConfig: Encodable { - - enum WidgetVisibility: String, Encodable { - case visible, hidden - - var isVisible: Bool { - self == .visible - } - } - - init(id: String, isVisible: Bool) { - self.id = id - self.visibility = isVisible ? .visible : .hidden - } - - var id: String - var visibility: WidgetVisibility - } - - struct Platform: Encodable { - var name: String - } -} - final class NewTabPageActionsManager: NewTabPageActionsManaging { private let appearancePreferences: AppearancePreferences private var cancellables = Set() - weak var userScript: NewTabPageUserScript? + private var userScripts = NSHashTable.weakObjects() init(appearancePreferences: AppearancePreferences) { self.appearancePreferences = appearancePreferences @@ -97,14 +63,20 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { .store(in: &cancellables) } + func registerUserScript(_ userScript: NewTabPageUserScript) { + userScripts.add(userScript) + } + private func notifyWidgetConfigsDidChange() { - userScript?.notifyWidgetConfigsDidChange(widgetConfigs: [ - .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), - .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) - ]) + userScripts.allObjects.forEach { userScript in + userScript.notifyWidgetConfigsDidChange(widgetConfigs: [ + .init(id: "favorites", isVisible: appearancePreferences.isFavoriteVisible), + .init(id: "privacyStats", isVisible: appearancePreferences.isRecentActivityVisible) + ]) + } } - var configuration: NewTabPageConfiguration { + var configuration: NewTabPageUserScript.NewTabPageConfiguration { #if DEBUG || REVIEW let env = "development" #else @@ -193,7 +165,7 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging { guard let id = param["id"], let visibility = param["visibility"] else { continue } - let isVisible = NewTabPageConfiguration.WidgetConfig.WidgetVisibility(rawValue: visibility)?.isVisible == true + let isVisible = NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig.WidgetVisibility(rawValue: visibility)?.isVisible == true switch id { case "favorites": appearancePreferences.isFavoriteVisible = isVisible diff --git a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift index edc1e6e315..7c70f45efe 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserContentController.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserContentController.swift @@ -26,8 +26,8 @@ final class NewTabPageUserContentController: WKUserContentController { let newTabPageUserScriptProvider: NewTabPageUserScriptProvider @MainActor - override init() { - newTabPageUserScriptProvider = NewTabPageUserScriptProvider() + init(newTabPageUserScript: NewTabPageUserScript) { + newTabPageUserScriptProvider = NewTabPageUserScriptProvider(newTabPageUserScript: newTabPageUserScript) super.init() @@ -54,9 +54,9 @@ final class NewTabPageUserScriptProvider: UserScriptsProvider { let specialPagesUserScript: SpecialPagesUserScript - init() { + init(newTabPageUserScript: NewTabPageUserScript) { specialPagesUserScript = SpecialPagesUserScript() - specialPagesUserScript.withNewTabPage() + specialPagesUserScript.registerSubfeature(delegate: newTabPageUserScript) } @MainActor @@ -80,7 +80,7 @@ final class NewTabPageUserScriptProvider: UserScriptsProvider { extension WKWebViewConfiguration { @MainActor - func applyNewTabPageWebViewConfiguration(with featureFlagger: FeatureFlagger) { + func applyNewTabPageWebViewConfiguration(with featureFlagger: FeatureFlagger, newTabPageUserScript: NewTabPageUserScript) { if urlSchemeHandler(forURLScheme: URL.NavigationalScheme.duck.rawValue) == nil { setURLSchemeHandler( DuckURLSchemeHandler(featureFlagger: featureFlagger), @@ -88,6 +88,6 @@ extension WKWebViewConfiguration { ) } preferences[.developerExtrasEnabled] = true - self.userContentController = NewTabPageUserContentController() + self.userContentController = NewTabPageUserContentController(newTabPageUserScript: newTabPageUserScript) } } diff --git a/DuckDuckGo/HomePage/NewTabPageUserScript.swift b/DuckDuckGo/HomePage/NewTabPageUserScript.swift index 45da589213..0a3f5ddc77 100644 --- a/DuckDuckGo/HomePage/NewTabPageUserScript.swift +++ b/DuckDuckGo/HomePage/NewTabPageUserScript.swift @@ -26,7 +26,7 @@ final class NewTabPageUserScript: NSObject, Subfeature { var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "newtab")]) let featureName: String = "newTabPage" weak var broker: UserScriptMessageBroker? - var webViews: NSHashTable = .weakObjects() + weak var webView: WKWebView? // MARK: - MessageNames enum MessageNames: String, CaseIterable { @@ -44,7 +44,7 @@ final class NewTabPageUserScript: NSObject, Subfeature { init(actionsManager: NewTabPageActionsManaging) { self.actionsManager = actionsManager super.init() - actionsManager.userScript = self + actionsManager.registerUserScript(self) } public func with(broker: UserScriptMessageBroker) { @@ -70,9 +70,10 @@ final class NewTabPageUserScript: NSObject, Subfeature { } func notifyWidgetConfigsDidChange(widgetConfigs: [NewTabPageConfiguration.WidgetConfig]) { - for webView in webViews.allObjects { - broker?.push(method: "widgets_onConfigUpdated", params: widgetConfigs, for: self, into: webView) + guard let webView else { + return } + broker?.push(method: "widgets_onConfigUpdated", params: widgetConfigs, for: self, into: webView) } } @@ -125,6 +126,41 @@ extension NewTabPageUserScript { extension NewTabPageUserScript { + struct NewTabPageConfiguration: Encodable { + var widgets: [Widget] + var widgetConfigs: [WidgetConfig] + var env: String + var locale: String + var platform: Platform + + struct Widget: Encodable { + var id: String + } + + struct WidgetConfig: Encodable { + + enum WidgetVisibility: String, Encodable { + case visible, hidden + + var isVisible: Bool { + self == .visible + } + } + + init(id: String, isVisible: Bool) { + self.id = id + self.visibility = isVisible ? .visible : .hidden + } + + var id: String + var visibility: WidgetVisibility + } + + struct Platform: Encodable { + var name: String + } + } + struct WidgetConfig: Encodable { let animation: Animation? let expansion: Expansion diff --git a/DuckDuckGo/HomePage/NewTabPageWebView.swift b/DuckDuckGo/HomePage/NewTabPageWebView.swift index 171d989a4d..31350c6a50 100644 --- a/DuckDuckGo/HomePage/NewTabPageWebView.swift +++ b/DuckDuckGo/HomePage/NewTabPageWebView.swift @@ -21,15 +21,16 @@ import WebKit final class NewTabPageWebView: WebView { - init(featureFlagger: FeatureFlagger) { + init(featureFlagger: FeatureFlagger, newTabPageUserScript: NewTabPageUserScript) { let configuration = WKWebViewConfiguration() - configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger) + configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger, newTabPageUserScript: newTabPageUserScript) super.init(frame: .zero, configuration: configuration) + newTabPageUserScript.webView = self navigationDelegate = self load(URLRequest(url: URL.newtab)) } - + @MainActor required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift index 6c3ead0f1b..f5cff41746 100644 --- a/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift +++ b/DuckDuckGo/Tab/Model/SpecialPagesUserScriptExtension.swift @@ -28,11 +28,6 @@ extension SpecialPagesUserScript { self.registerSubfeature(delegate: onboardingScript) } - @MainActor - func withNewTabPage() { - self.registerSubfeature(delegate: NSApp.delegateTyped.newTabPageUserScript) - } - func withDuckPlayerIfAvailable() { var youtubePlayerUserScript: YoutubePlayerUserScript? if DuckPlayer.shared.isAvailable { diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 82ca8775f8..6ad44ae935 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -42,12 +42,9 @@ final class BrowserTabViewController: NSViewController { private lazy var hoverLabel = NSTextField(string: URL.duckDuckGo.absoluteString) private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) - private let newTabPageUserScript: NewTabPageUserScript - private(set) lazy var newTabPageWebView: WebView = { - let webView = NewTabPageWebView(featureFlagger: featureFlagger) - newTabPageUserScript.webViews.add(webView) - return webView - }() + private let newTabPageActionsManager: NewTabPageActionsManaging + private lazy var newTabPageUserScript: NewTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) + private lazy var newTabPageWebView: WebView = NewTabPageWebView(featureFlagger: featureFlagger, newTabPageUserScript: newTabPageUserScript) private(set) weak var webView: WebView? private weak var webViewContainer: NSView? private weak var webViewSnapshot: NSView? @@ -91,14 +88,14 @@ final class BrowserTabViewController: NSViewController { onboardingDialogTypeProvider: ContextualOnboardingDialogTypeProviding & ContextualOnboardingStateUpdater = Application.appDelegate.onboardingStateMachine, onboardingDialogFactory: ContextualDaxDialogsFactory = DefaultContextualDaxDialogViewFactory(), featureFlagger: FeatureFlagger = NSApp.delegateTyped.featureFlagger, - newTabPageUserScript: NewTabPageUserScript = NSApp.delegateTyped.newTabPageUserScript + newTabPageActionsManager: NewTabPageActionsManaging = NSApp.delegateTyped.newTabPageActionsManager ) { self.tabCollectionViewModel = tabCollectionViewModel self.bookmarkManager = bookmarkManager self.onboardingDialogTypeProvider = onboardingDialogTypeProvider self.onboardingDialogFactory = onboardingDialogFactory self.featureFlagger = featureFlagger - self.newTabPageUserScript = newTabPageUserScript + self.newTabPageActionsManager = newTabPageActionsManager containerStackView = NSStackView() super.init(nibName: nil, bundle: nil) From 53e787edc45cadfd3d4ed9b6cafba0792515de1c Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 19 Nov 2024 09:40:14 +0100 Subject: [PATCH 41/42] Add NewTabPageViewModel --- DuckDuckGo.xcodeproj/project.pbxproj | 12 ++++---- ...iew.swift => NewTabPageWebViewModel.swift} | 28 +++++++++++-------- .../Tab/View/BrowserTabViewController.swift | 7 ++--- DuckDuckGo/Tab/View/WebView.swift | 22 +++++++-------- 4 files changed, 37 insertions(+), 32 deletions(-) rename DuckDuckGo/HomePage/{NewTabPageWebView.swift => NewTabPageWebViewModel.swift} (61%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 929283e6e2..e080aa1d38 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1135,8 +1135,8 @@ 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA2281132CB002621E7 /* TabLazyLoaderDataSource.swift */; }; 37534CA52811987D002621E7 /* AdjacentItemEnumeratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA42811987D002621E7 /* AdjacentItemEnumeratorTests.swift */; }; 37534CA8281198CD002621E7 /* AdjacentItemEnumerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */; }; - 3758CBAB2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */; }; - 3758CBAC2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */; }; + 3758CBAB2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758CBAA2CE63D510089FC2D /* NewTabPageWebViewModel.swift */; }; + 3758CBAC2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758CBAA2CE63D510089FC2D /* NewTabPageWebViewModel.swift */; }; 376113CC2B29CD5B00E794BB /* CriticalPathsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 565E46DF2B2725DD0013AC2A /* CriticalPathsTests.swift */; }; 376705AF27EB488600DD8D76 /* RoundedSelectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511B3262CAA5A00F6079C /* RoundedSelectionRowView.swift */; }; 376731822C7E226A00EB097B /* HomePageViewBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731812C7E226A00EB097B /* HomePageViewBackground.swift */; }; @@ -3616,7 +3616,7 @@ 37534CA2281132CB002621E7 /* TabLazyLoaderDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLazyLoaderDataSource.swift; sourceTree = ""; }; 37534CA42811987D002621E7 /* AdjacentItemEnumeratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjacentItemEnumeratorTests.swift; sourceTree = ""; }; 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdjacentItemEnumerator.swift; sourceTree = ""; }; - 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageWebView.swift; sourceTree = ""; }; + 3758CBAA2CE63D510089FC2D /* NewTabPageWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageWebViewModel.swift; sourceTree = ""; }; 376113C52B29BCD600E794BB /* SyncE2EUITests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITests.xcconfig; sourceTree = ""; }; 376113D42B29CD5B00E794BB /* SyncE2EUITests App Store.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "SyncE2EUITests App Store.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 376113D72B29D0F800E794BB /* SyncE2EUITestsAppStore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = SyncE2EUITestsAppStore.xcconfig; sourceTree = ""; }; @@ -8767,7 +8767,7 @@ 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */, 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */, 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */, - 3758CBAA2CE63D510089FC2D /* NewTabPageWebView.swift */, + 3758CBAA2CE63D510089FC2D /* NewTabPageWebViewModel.swift */, 85589E8527BBB8DD0038AD11 /* Model */, AAE71DB325F66A3F00D74437 /* View */, 85AC7ADA27BD628400FFB69B /* HomePage.swift */, @@ -11661,7 +11661,7 @@ 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 37D0469E2C7D0EDD00AEAA50 /* CustomBackground.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, - 3758CBAB2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */, + 3758CBAB2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */, 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 3706FBD0293F65D500E42796 /* WebKitVersionProvider.swift in Sources */, @@ -13082,7 +13082,7 @@ AA68C3D32490ED62001B8783 /* NavigationBarViewController.swift in Sources */, AA585DAF2490E6E600E9A3E2 /* MainViewController.swift in Sources */, F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, - 3758CBAC2CE63D540089FC2D /* NewTabPageWebView.swift in Sources */, + 3758CBAC2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */, 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */, AA5FA69A275F91C700DCE9C9 /* Favicon.swift in Sources */, AABEE69A24A902A90043105B /* SuggestionContainerViewModel.swift in Sources */, diff --git a/DuckDuckGo/HomePage/NewTabPageWebView.swift b/DuckDuckGo/HomePage/NewTabPageWebViewModel.swift similarity index 61% rename from DuckDuckGo/HomePage/NewTabPageWebView.swift rename to DuckDuckGo/HomePage/NewTabPageWebViewModel.swift index 31350c6a50..0d083fa331 100644 --- a/DuckDuckGo/HomePage/NewTabPageWebView.swift +++ b/DuckDuckGo/HomePage/NewTabPageWebViewModel.swift @@ -1,5 +1,5 @@ // -// NewTabPageWebView.swift +// NewTabPageWebViewModel.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,24 +19,30 @@ import BrowserServicesKit import WebKit -final class NewTabPageWebView: WebView { +/** + * This class manages + */ +@MainActor +final class NewTabPageWebViewModel: NSObject { + let newTabPageUserScript: NewTabPageUserScript + let webView: WebView + + init(featureFlagger: FeatureFlagger, actionsManager: NewTabPageActionsManaging) { + newTabPageUserScript = NewTabPageUserScript(actionsManager: actionsManager) - init(featureFlagger: FeatureFlagger, newTabPageUserScript: NewTabPageUserScript) { let configuration = WKWebViewConfiguration() configuration.applyNewTabPageWebViewConfiguration(with: featureFlagger, newTabPageUserScript: newTabPageUserScript) + webView = WebView(frame: .zero, configuration: configuration) - super.init(frame: .zero, configuration: configuration) - newTabPageUserScript.webView = self - navigationDelegate = self - load(URLRequest(url: URL.newtab)) - } + super.init() - @MainActor required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + webView.navigationDelegate = self + webView.load(URLRequest(url: URL.newtab)) + newTabPageUserScript.webView = webView } } -extension NewTabPageWebView: WKNavigationDelegate { +extension NewTabPageWebViewModel: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { navigationAction.request.url == .newtab ? .allow : .cancel } diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 6ad44ae935..300eb63804 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -43,8 +43,7 @@ final class BrowserTabViewController: NSViewController { private lazy var hoverLabelContainer = ColorView(frame: .zero, backgroundColor: .browserTabBackground, borderWidth: 0) private let newTabPageActionsManager: NewTabPageActionsManaging - private lazy var newTabPageUserScript: NewTabPageUserScript = NewTabPageUserScript(actionsManager: newTabPageActionsManager) - private lazy var newTabPageWebView: WebView = NewTabPageWebView(featureFlagger: featureFlagger, newTabPageUserScript: newTabPageUserScript) + private(set) lazy var newTabPageWebViewModel: NewTabPageWebViewModel = NewTabPageWebViewModel(featureFlagger: featureFlagger, actionsManager: newTabPageActionsManager) private(set) weak var webView: WebView? private weak var webViewContainer: NSView? private weak var webViewSnapshot: NSView? @@ -544,7 +543,7 @@ final class BrowserTabViewController: NSViewController { } func displayWebView(of tabViewModel: TabViewModel) { - let newWebView = tabViewModel.tab.content == .newtab ? newTabPageWebView : tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content == .newtab ? newTabPageWebViewModel.webView : tabViewModel.tab.webView cleanUpRemoteWebViewIfNeeded(newWebView) webView = newWebView @@ -846,7 +845,7 @@ final class BrowserTabViewController: NSViewController { return false } - let newWebView = tabViewModel.tab.content == .newtab ? newTabPageWebView : tabViewModel.tab.webView + let newWebView = tabViewModel.tab.content == .newtab ? newTabPageWebViewModel.webView : tabViewModel.tab.webView let isPinnedTab = tabCollectionViewModel.pinnedTabsCollection?.tabs.contains(tabViewModel.tab) == true let isKeyWindow = view.window?.isKeyWindow == true diff --git a/DuckDuckGo/Tab/View/WebView.swift b/DuckDuckGo/Tab/View/WebView.swift index 83c7a65910..f421f32037 100644 --- a/DuckDuckGo/Tab/View/WebView.swift +++ b/DuckDuckGo/Tab/View/WebView.swift @@ -39,7 +39,7 @@ protocol WebViewZoomLevelDelegate: AnyObject { } @objc(DuckDuckGo_WebView) -open class WebView: WKWebView { +final class WebView: WKWebView { weak var contextMenuDelegate: WebViewContextMenuDelegate? weak var interactionEventsDelegate: WebViewInteractionEventsDelegate? @@ -53,7 +53,7 @@ open class WebView: WKWebView { isInspectorShown && window != nil } - open override func addTrackingArea(_ trackingArea: NSTrackingArea) { + override func addTrackingArea(_ trackingArea: NSTrackingArea) { /// disable mouseEntered/mouseMoved/mouseExited events passing to Web View while it‘s loading /// see https://app.asana.com/0/1177771139624306/1206990108527681/f if trackingArea.owner?.className == "WKMouseTrackingObserver" { @@ -75,7 +75,7 @@ open class WebView: WKWebView { super.addTrackingArea(trackingArea) } - open override var isInFullScreenMode: Bool { + override var isInFullScreenMode: Bool { if #available(macOS 13.0, *) { return self.fullscreenState != .notInFullscreen } else { @@ -160,29 +160,29 @@ open class WebView: WKWebView { // MARK: - Menu - open override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { + override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { super.willOpenMenu(menu, with: event) contextMenuDelegate?.webView(self, willOpenContextMenu: menu, with: event) } - open override func didCloseMenu(_ menu: NSMenu, with event: NSEvent?) { + override func didCloseMenu(_ menu: NSMenu, with event: NSEvent?) { super.didCloseMenu(menu, with: event) contextMenuDelegate?.webView(self, didCloseContextMenu: menu, with: event) } // MARK: - Events - open override func mouseDown(with event: NSEvent) { + override func mouseDown(with event: NSEvent) { super.mouseDown(with: event) interactionEventsDelegate?.webView(self, mouseDown: event) } - open override func keyDown(with event: NSEvent) { + override func keyDown(with event: NSEvent) { super.keyDown(with: event) interactionEventsDelegate?.webView(self, keyDown: event) } - open override func scrollWheel(with event: NSEvent) { + override func scrollWheel(with event: NSEvent) { super.scrollWheel(with: event) interactionEventsDelegate?.webView(self, scrollWheel: event) } @@ -198,7 +198,7 @@ open class WebView: WKWebView { } } - open override func viewDidMoveToWindow() { + override func viewDidMoveToWindow() { super.viewDidMoveToWindow() if shouldShowWebInspector { openDeveloperTools() @@ -268,7 +268,7 @@ open class WebView: WKWebView { // MARK: - NSDraggingDestination - open override func draggingUpdated(_ draggingInfo: NSDraggingInfo) -> NSDragOperation { + override func draggingUpdated(_ draggingInfo: NSDraggingInfo) -> NSDragOperation { if draggingInfo.draggingSource is WebView { return super.draggingUpdated(draggingInfo) } @@ -285,7 +285,7 @@ open class WebView: WKWebView { return superview.draggingUpdated(draggingInfo) } - open override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { + override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { if draggingInfo.draggingSource is WebView { return super.performDragOperation(draggingInfo) } From 555053c44308455575d513d3c9bc2eeb25421f06 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 19 Nov 2024 09:52:57 +0100 Subject: [PATCH 42/42] Update comment --- DuckDuckGo/HomePage/NewTabPageWebViewModel.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/HomePage/NewTabPageWebViewModel.swift b/DuckDuckGo/HomePage/NewTabPageWebViewModel.swift index 0d083fa331..d61fa97f76 100644 --- a/DuckDuckGo/HomePage/NewTabPageWebViewModel.swift +++ b/DuckDuckGo/HomePage/NewTabPageWebViewModel.swift @@ -20,7 +20,14 @@ import BrowserServicesKit import WebKit /** - * This class manages + * This class manages a dedicated web view for displaying New Tab Page. + * + * It initializes NTP user script, the NTP-specific web view configuration + * and then sets up a new web view with that configuration. It also serves + * as a navigation delegate for the web view, blocking all navigations other than + * to the New Tab Page. + * + * This class is inspired by `DBPUIViewModel`. */ @MainActor final class NewTabPageWebViewModel: NSObject {