From 91738a9d2876fb64409c90a480ea0d2530b5338e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 29 Nov 2024 23:33:11 +0100 Subject: [PATCH 01/62] Add NewTabPageNextStepsCardsClient --- DuckDuckGo.xcodeproj/project.pbxproj | 14 ++ .../Model/HomePageContinueSetUpModel.swift | 41 +++- .../HomePage/View/ContinueSetUpView.swift | 2 +- .../HomePageSettingsView.swift | 4 +- .../View/HomePageViewController.swift | 2 +- .../NewTabPage/NewTabPageActionsManager.swift | 8 + .../NewTabPageConfigurationClient.swift | 21 +- .../NewTabPageNextStepsCardsClient.swift | 214 ++++++++++++++++++ .../Model/AppearancePreferences.swift | 6 + 9 files changed, 295 insertions(+), 17 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index df3cff620f..26a4c7fa7d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1248,6 +1248,8 @@ 37BF3F22286F0A7A00BD9014 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; 37C9F78C2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; 37C9F78D2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; + 37CB368B2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */; }; + 37CB368C2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */; }; 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; 37CC53EC27E8A4D10028713D /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; @@ -3761,6 +3763,7 @@ 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsTabExtension.swift; sourceTree = ""; }; + 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsClient.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; 37CC53F327E8D4620028713D /* NSPathControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPathControlView.swift; sourceTree = ""; }; @@ -5857,6 +5860,7 @@ children = ( 37F8E2322CEE363D002F0141 /* NewTabPageContextMenuPresenting.swift */, 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */, + 37CB36892CFA5ADA00E5E5FA /* NextStepsCards */, 37F1E32D2CEF2DDD00130142 /* Favorites */, 37DF370B2CF3CEF5005ED34B /* PrivacyStats */, 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */, @@ -6039,6 +6043,14 @@ path = PrivacyStats; sourceTree = ""; }; + 37CB36892CFA5ADA00E5E5FA /* NextStepsCards */ = { + isa = PBXGroup; + children = ( + 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */, + ); + path = NextStepsCards; + sourceTree = ""; + }; 37CD54C027F2FDD100F1F7B9 /* Model */ = { isa = PBXGroup; children = ( @@ -12168,6 +12180,7 @@ 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultReporter.swift in Sources */, + 37CB368C2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */, 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */, 3706FEBD293F6EFF00E42796 /* BWCommand.swift in Sources */, C172E7302C9329D300521D9A /* FlippedView.swift in Sources */, @@ -13083,6 +13096,7 @@ B62B483E2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, 4B92929B26670D2A00AD2C21 /* BookmarkOutlineViewDataSource.swift in Sources */, 56D145EB29E6C99B00E3488A /* DataImportStatusProviding.swift in Sources */, + 37CB368B2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */, 843965122C6F2FFE004C8899 /* NSDragOperationExtension.swift in Sources */, 31D5375C291D944100407A95 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D9297BF2C1B062900A38521 /* ApplicationUpdateDetector.swift in Sources */, diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index c457c86b13..16c96f0671 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -26,6 +26,20 @@ import Subscription import NetworkProtection import NetworkProtectionUI +protocol ContinueSetUpModelTabOpening { + @MainActor + func openTab(_ tab: Tab) +} + +struct TabCollectionViewModelTabOpener: ContinueSetUpModelTabOpening { + let tabCollectionViewModel: TabCollectionViewModel + + @MainActor + func openTab(_ tab: Tab) { + tabCollectionViewModel.insertOrAppend(tab: tab, selected: true) + } +} + extension HomePage.Models { static let newHomePageTabOpen = Notification.Name("newHomePageAppOpen") @@ -49,14 +63,16 @@ extension HomePage.Models { private let defaultBrowserProvider: DefaultBrowserProvider private let dockCustomizer: DockCustomization private let dataImportProvider: DataImportStatusProviding - private let tabCollectionViewModel: TabCollectionViewModel + private let tabOpener: ContinueSetUpModelTabOpening private let emailManager: EmailManager private let duckPlayerPreferences: DuckPlayerPreferencesPersistor private let subscriptionManager: SubscriptionManager - @UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) - var shouldShowAllFeatures: Bool { + @Published + var shouldShowAllFeatures: Bool = UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false).wrappedValue { didSet { + let udWrapper = UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) + udWrapper.wrappedValue = shouldShowAllFeatures updateVisibleMatrix() } } @@ -102,7 +118,7 @@ extension HomePage.Models { lazy var listOfFeatures = settings.isFirstSession ? firstRunFeatures : randomisedFeatures - private var featuresMatrix: [[FeatureType]] = [[]] { + @Published var featuresMatrix: [[FeatureType]] = [[]] { didSet { updateVisibleMatrix() } @@ -110,18 +126,19 @@ extension HomePage.Models { @Published var visibleFeaturesMatrix: [[FeatureType]] = [[]] - init(defaultBrowserProvider: DefaultBrowserProvider, - dockCustomizer: DockCustomization, - dataImportProvider: DataImportStatusProviding, - tabCollectionViewModel: TabCollectionViewModel, + init(defaultBrowserProvider: DefaultBrowserProvider = SystemDefaultBrowserProvider(), + dockCustomizer: DockCustomization = DockCustomizer(), + dataImportProvider: DataImportStatusProviding = BookmarksAndPasswordsImportStatusProvider(), + tabOpener: ContinueSetUpModelTabOpening, emailManager: EmailManager = EmailManager(), - duckPlayerPreferences: DuckPlayerPreferencesPersistor, + duckPlayerPreferences: DuckPlayerPreferencesPersistor = DuckPlayerPreferencesUserDefaultsPersistor(), privacyConfigurationManager: PrivacyConfigurationManaging = AppPrivacyFeatures.shared.contentBlocking.privacyConfigurationManager, subscriptionManager: SubscriptionManager = Application.appDelegate.subscriptionManager) { + self.defaultBrowserProvider = defaultBrowserProvider self.dockCustomizer = dockCustomizer self.dataImportProvider = dataImportProvider - self.tabCollectionViewModel = tabCollectionViewModel + self.tabOpener = tabOpener self.emailManager = emailManager self.duckPlayerPreferences = duckPlayerPreferences self.privacyConfigurationManager = privacyConfigurationManager @@ -166,14 +183,14 @@ extension HomePage.Models { private func performDuckPlayerAction() { if let videoUrl = URL(string: duckPlayerURL) { let tab = Tab(content: .url(videoUrl, source: .link), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) + tabOpener.openTab(tab) } } @MainActor private func performEmailProtectionAction() { let tab = Tab(content: .url(EmailUrls().emailProtectionLink, source: .ui), shouldLoadInBackground: true) - tabCollectionViewModel.append(tab: tab) + tabOpener.openTab(tab) } func performDockAction() { diff --git a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift index 33fa94a8f1..3c48f090b6 100644 --- a/DuckDuckGo/HomePage/View/ContinueSetUpView.swift +++ b/DuckDuckGo/HomePage/View/ContinueSetUpView.swift @@ -292,7 +292,7 @@ extension HomePage.Views { defaultBrowserProvider: SystemDefaultBrowserProvider(), dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), - tabCollectionViewModel: TabCollectionViewModel(), + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: TabCollectionViewModel()), duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor() )) } diff --git a/DuckDuckGo/HomePage/View/HomePageSettings/HomePageSettingsView.swift b/DuckDuckGo/HomePage/View/HomePageSettings/HomePageSettingsView.swift index 2ea65f9003..cf1086c16a 100644 --- a/DuckDuckGo/HomePage/View/HomePageSettings/HomePageSettingsView.swift +++ b/DuckDuckGo/HomePage/View/HomePageSettings/HomePageSettingsView.swift @@ -247,7 +247,7 @@ extension HomePage.Views.BackgroundCategoryView { defaultBrowserProvider: SystemDefaultBrowserProvider(), dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), - tabCollectionViewModel: TabCollectionViewModel(), + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: TabCollectionViewModel()), duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor() )) .environmentObject(HomePage.Models.FavoritesModel( @@ -276,7 +276,7 @@ extension HomePage.Views.BackgroundCategoryView { defaultBrowserProvider: SystemDefaultBrowserProvider(), dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), - tabCollectionViewModel: TabCollectionViewModel(), + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: TabCollectionViewModel()), duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor() )) .environmentObject(HomePage.Models.FavoritesModel( diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 04635afce2..7651f0b5a5 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -171,7 +171,7 @@ final class HomePageViewController: NSViewController { defaultBrowserProvider: SystemDefaultBrowserProvider(), dockCustomizer: DockCustomizer(), dataImportProvider: BookmarksAndPasswordsImportStatusProvider(), - tabCollectionViewModel: tabCollectionViewModel, + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: tabCollectionViewModel), duckPlayerPreferences: DuckPlayerPreferencesUserDefaultsPersistor() ) } diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift index 90fd7de527..474eda2e8b 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift @@ -130,8 +130,16 @@ extension NewTabPageActionsManager { self.init(scriptClients: [ NewTabPageConfigurationClient(appearancePreferences: appearancePreferences), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel, openURLHandler: openURLHandler), + NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), NewTabPageFavoritesClient(favoritesModel: NewTabPageFavoritesModel()), NewTabPagePrivacyStatsClient(model: privacyStatsModel) ]) } } + +struct NewTabPageTabOpener: ContinueSetUpModelTabOpening { + @MainActor + func openTab(_ tab: Tab) { + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.insertOrAppend(tab: tab, selected: true) + } +} diff --git a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift b/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift index 8eb5c7b7d8..6d3020cf27 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift @@ -37,6 +37,13 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { self.appearancePreferences = appearancePreferences self.contextMenuPresenter = contextMenuPresenter + appearancePreferences.isContinueSetUpVisiblePublisher.removeDuplicates().asVoid() + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.notifyWidgetConfigsDidChange() + } + .store(in: &cancellables) + appearancePreferences.$isFavoriteVisible.dropFirst().removeDuplicates().asVoid() .receive(on: DispatchQueue.main) .sink { [weak self] in @@ -73,6 +80,7 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { private func notifyWidgetConfigsDidChange() { let widgetConfigs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ + .init(id: .nextSteps, isVisible: appearancePreferences.isContinueSetUpVisible), .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) ] @@ -88,6 +96,11 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { for menuItem in params.visibilityMenuItems { switch menuItem.id { + case .nextSteps: + let item = NSMenuItem(title: menuItem.title, action: #selector(toggleVisibility(_:)), representedObject: menuItem.id) + .targetting(self) + item.state = appearancePreferences.isContinueSetUpVisible ? .on : .off + menu.addItem(item) case .favorites: let item = NSMenuItem(title: menuItem.title, action: #selector(toggleVisibility(_:)), representedObject: menuItem.id) .targetting(self) @@ -112,6 +125,8 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { @objc private func toggleVisibility(_ sender: NSMenuItem) { switch sender.representedObject as? NewTabPageUserScript.WidgetId { + case .nextSteps: + appearancePreferences.isContinueSetUpVisible.toggle() case .favorites: appearancePreferences.isFavoriteVisible.toggle() case .privacyStats: @@ -131,10 +146,12 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { return NewTabPageUserScript.NewTabPageConfiguration( widgets: [ .init(id: .rmf), + .init(id: .nextSteps), .init(id: .favorites), .init(id: .privacyStats) ], widgetConfigs: [ + .init(id: .nextSteps, isVisible: appearancePreferences.isContinueSetUpVisible), .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) ], @@ -151,6 +168,8 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { } for widgetConfig in widgetConfigs { switch widgetConfig.id { + case .nextSteps: + appearancePreferences.isContinueSetUpVisible = widgetConfig.visibility.isVisible case .favorites: appearancePreferences.isFavoriteVisible = widgetConfig.visibility.isVisible case .privacyStats: @@ -174,7 +193,7 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { extension NewTabPageUserScript { enum WidgetId: String, Codable { - case rmf, favorites, privacyStats + case rmf, nextSteps, favorites, privacyStats } struct ContextMenuParams: Codable { diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift new file mode 100644 index 0000000000..7e9e8fa61c --- /dev/null +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -0,0 +1,214 @@ +// +// NewTabPageNextStepsCardsClient.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 Bookmarks +import Common +import Combine +import UserScript + +protocol NewTabPageNextStepsCardsProviding: AnyObject { + var isViewExpanded: Bool { get set } + var isViewExpandedPublisher: AnyPublisher { get } + + var cards: [NewTabPageNextStepsCardsClient.CardID] { get } + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } + + @MainActor + func performAction(for card: NewTabPageNextStepsCardsClient.CardID) + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) +} + +extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding { + var isViewExpanded: Bool { + get { + shouldShowAllFeatures + } + set { + shouldShowAllFeatures = newValue + } + } + + var isViewExpandedPublisher: AnyPublisher { + $shouldShowAllFeatures.dropFirst().eraseToAnyPublisher() + } + + var cards: [NewTabPageNextStepsCardsClient.CardID] { + featuresMatrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } + } + + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { + $featuresMatrix.dropFirst() + .map { matrix in + matrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } + } + .eraseToAnyPublisher() + } + + @MainActor + func performAction(for card: NewTabPageNextStepsCardsClient.CardID) { + performAction(for: .init(card)) + } + + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { + removeItem(for: .init(card)) + } +} + +extension HomePage.Models.FeatureType { + init(_ card: NewTabPageNextStepsCardsClient.CardID) { + switch card { + case .bringStuff: + self = .importBookmarksAndPasswords + case .defaultApp: + self = .defaultBrowser + case .emailProtection: + self = .emailProtection + case .duckplayer: + self = .duckplayer + case .addAppToDockMac: + self = .dock + } + } +} + +final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { + + let model: NewTabPageNextStepsCardsProviding + weak var userScriptsSource: NewTabPageUserScriptsSource? + private var cancellables: Set = [] + + init(model: NewTabPageNextStepsCardsProviding) { + self.model = model + + model.cardsPublisher + .sink { [weak self] cardIDs in + Task { @MainActor in + self?.notifyDataUpdated(cardIDs) + } + } + .store(in: &cancellables) + + model.isViewExpandedPublisher + .sink { [weak self] showAllCards in + Task { @MainActor in + self?.notifyConfigUpdated(showAllCards) + } + } + .store(in: &cancellables) + } + + enum MessageName: String, CaseIterable { + case action = "nextSteps_action" + case dismiss = "nextSteps_dismiss" + case getConfig = "nextSteps_getConfig" + case getData = "nextSteps_getData" + case onConfigUpdate = "nextSteps_onConfigUpdate" + case onDataUpdate = "nextSteps_onDataUpdate" + case setConfig = "nextSteps_setConfig" + } + + func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + userScript.registerMessageHandlers([ + MessageName.action.rawValue: { [weak self] in try await self?.action(params: $0, original: $1) }, + MessageName.dismiss.rawValue: { [weak self] in try await self?.dismiss(params: $0, original: $1) }, + MessageName.getConfig.rawValue: { [weak self] in try await self?.getConfig(params: $0, original: $1) }, + MessageName.getData.rawValue: { [weak self] in try await self?.getData(params: $0, original: $1) }, + MessageName.setConfig.rawValue: { [weak self] in try await self?.setConfig(params: $0, original: $1) } + ]) + } + + func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let card: NewTabPageNextStepsCardsClient.Card = DecodableHelper.decode(from: params) else { + return nil + } + await model.performAction(for: card.id) + return nil + } + + func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let card: NewTabPageNextStepsCardsClient.Card = DecodableHelper.decode(from: params) else { + return nil + } + model.dismiss(card.id) + return nil + } + + func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + let expansion: NewTabPageUserScript.WidgetConfig.Expansion = model.isViewExpanded ? .expanded : .collapsed + return NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) + } + + @MainActor + func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { + return nil + } + model.isViewExpanded = config.expansion == .expanded + return nil + } + + @MainActor + func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + let cards = model.cards.map(NewTabPageNextStepsCardsClient.Card.init(id:)) + return cards.isEmpty ? nil : cards + } + + @MainActor + func notifyDataUpdated(_ cardIDs: [NewTabPageNextStepsCardsClient.CardID]) { + let cards = cardIDs.map(NewTabPageNextStepsCardsClient.Card.init(id:)) + let params = cards.isEmpty ? nil : cards + pushMessage(named: MessageName.onDataUpdate.rawValue, params: params) + } + + @MainActor + private func notifyConfigUpdated(_ showAllCards: Bool) { + let expansion: NewTabPageUserScript.WidgetConfig.Expansion = showAllCards ? .expanded : .collapsed + let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) + pushMessage(named: MessageName.onConfigUpdate.rawValue, params: config) + } +} + +extension NewTabPageNextStepsCardsClient { + + enum CardID: String, Codable { + case bringStuff + case defaultApp + case emailProtection + case duckplayer + case addAppToDockMac + + init(_ feature: HomePage.Models.FeatureType) { + switch feature { + case .duckplayer: + self = .duckplayer + case .emailProtection: + self = .emailProtection + case .defaultBrowser: + self = .defaultApp + case .dock: + self = .addAppToDockMac + case .importBookmarksAndPasswords: + self = .bringStuff + } + } + } + + struct Card: Codable { + let id: CardID + } +} diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index 43fc227272..371009da4a 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -19,6 +19,7 @@ import Foundation import AppKit import Bookmarks +import Combine import Common import PixelKit import os.log @@ -242,10 +243,14 @@ final class AppearancePreferences: ObservableObject { if !isContinueSetUpVisible { PixelKit.fire(GeneralPixel.continueSetUpSectionHidden) } + isContinueSetUpVisibleSubject.send(newValue) self.objectWillChange.send() } } + let isContinueSetUpVisiblePublisher: AnyPublisher + private var isContinueSetUpVisibleSubject = PassthroughSubject() + func continueSetUpCardsViewDidAppear() { guard isContinueSetUpVisible, !isContinueSetUpCardsViewOutdated else { return } @@ -344,6 +349,7 @@ final class AppearancePreferences: ObservableObject { self.dateTimeProvider = dateTimeProvider self.isContinueSetUpCardsViewOutdated = persistor.continueSetUpCardsNumberOfDaysDemonstrated >= Constants.dismissNextStepsCardsAfterDays self.continueSetUpCardsClosed = persistor.continueSetUpCardsClosed + self.isContinueSetUpVisiblePublisher = isContinueSetUpVisibleSubject.eraseToAnyPublisher() currentThemeName = .init(rawValue: persistor.currentThemeName) ?? .systemDefault showFullURL = persistor.showFullURL favoritesDisplayMode = persistor.favoritesDisplayMode.flatMap(FavoritesDisplayMode.init) ?? .default From 4f1af62d708a5d2f4f6ea04de84aa3e73b5cf69a Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Sat, 30 Nov 2024 08:29:15 +0100 Subject: [PATCH 02/62] Wrap cards in NextStepsData struct --- .../NewTabPageNextStepsCardsClient.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index 7e9e8fa61c..fbfba57037 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -141,7 +141,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let card: NewTabPageNextStepsCardsClient.Card = DecodableHelper.decode(from: params) else { + guard let card: Card = DecodableHelper.decode(from: params) else { return nil } model.dismiss(card.id) @@ -164,14 +164,14 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { @MainActor func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let cards = model.cards.map(NewTabPageNextStepsCardsClient.Card.init(id:)) - return cards.isEmpty ? nil : cards + let cards = model.cards.map(Card.init(id:)) + return NextStepsData(content: cards.isEmpty ? nil : cards) } @MainActor - func notifyDataUpdated(_ cardIDs: [NewTabPageNextStepsCardsClient.CardID]) { - let cards = cardIDs.map(NewTabPageNextStepsCardsClient.Card.init(id:)) - let params = cards.isEmpty ? nil : cards + func notifyDataUpdated(_ cardIDs: [CardID]) { + let cards = cardIDs.map(Card.init(id:)) + let params = NextStepsData(content: cards.isEmpty ? nil : cards) pushMessage(named: MessageName.onDataUpdate.rawValue, params: params) } @@ -211,4 +211,8 @@ extension NewTabPageNextStepsCardsClient { struct Card: Codable { let id: CardID } + + struct NextStepsData: Codable { + let content: [Card]? + } } From 067c72d073fa54190837ef80eb87c010c8180e26 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Sun, 1 Dec 2024 18:17:24 +0100 Subject: [PATCH 03/62] Remove Next Steps Cards from HTML NTP customize menu --- .../NewTabPageConfigurationClient.swift | 18 ------------------ .../Model/AppearancePreferences.swift | 6 ------ 2 files changed, 24 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift b/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift index 6d3020cf27..41f88e5f84 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift @@ -37,13 +37,6 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { self.appearancePreferences = appearancePreferences self.contextMenuPresenter = contextMenuPresenter - appearancePreferences.isContinueSetUpVisiblePublisher.removeDuplicates().asVoid() - .receive(on: DispatchQueue.main) - .sink { [weak self] in - self?.notifyWidgetConfigsDidChange() - } - .store(in: &cancellables) - appearancePreferences.$isFavoriteVisible.dropFirst().removeDuplicates().asVoid() .receive(on: DispatchQueue.main) .sink { [weak self] in @@ -80,7 +73,6 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { private func notifyWidgetConfigsDidChange() { let widgetConfigs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ - .init(id: .nextSteps, isVisible: appearancePreferences.isContinueSetUpVisible), .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) ] @@ -96,11 +88,6 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { for menuItem in params.visibilityMenuItems { switch menuItem.id { - case .nextSteps: - let item = NSMenuItem(title: menuItem.title, action: #selector(toggleVisibility(_:)), representedObject: menuItem.id) - .targetting(self) - item.state = appearancePreferences.isContinueSetUpVisible ? .on : .off - menu.addItem(item) case .favorites: let item = NSMenuItem(title: menuItem.title, action: #selector(toggleVisibility(_:)), representedObject: menuItem.id) .targetting(self) @@ -125,8 +112,6 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { @objc private func toggleVisibility(_ sender: NSMenuItem) { switch sender.representedObject as? NewTabPageUserScript.WidgetId { - case .nextSteps: - appearancePreferences.isContinueSetUpVisible.toggle() case .favorites: appearancePreferences.isFavoriteVisible.toggle() case .privacyStats: @@ -151,7 +136,6 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { .init(id: .privacyStats) ], widgetConfigs: [ - .init(id: .nextSteps, isVisible: appearancePreferences.isContinueSetUpVisible), .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) ], @@ -168,8 +152,6 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { } for widgetConfig in widgetConfigs { switch widgetConfig.id { - case .nextSteps: - appearancePreferences.isContinueSetUpVisible = widgetConfig.visibility.isVisible case .favorites: appearancePreferences.isFavoriteVisible = widgetConfig.visibility.isVisible case .privacyStats: diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index 371009da4a..43fc227272 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -19,7 +19,6 @@ import Foundation import AppKit import Bookmarks -import Combine import Common import PixelKit import os.log @@ -243,14 +242,10 @@ final class AppearancePreferences: ObservableObject { if !isContinueSetUpVisible { PixelKit.fire(GeneralPixel.continueSetUpSectionHidden) } - isContinueSetUpVisibleSubject.send(newValue) self.objectWillChange.send() } } - let isContinueSetUpVisiblePublisher: AnyPublisher - private var isContinueSetUpVisibleSubject = PassthroughSubject() - func continueSetUpCardsViewDidAppear() { guard isContinueSetUpVisible, !isContinueSetUpCardsViewOutdated else { return } @@ -349,7 +344,6 @@ final class AppearancePreferences: ObservableObject { self.dateTimeProvider = dateTimeProvider self.isContinueSetUpCardsViewOutdated = persistor.continueSetUpCardsNumberOfDaysDemonstrated >= Constants.dismissNextStepsCardsAfterDays self.continueSetUpCardsClosed = persistor.continueSetUpCardsClosed - self.isContinueSetUpVisiblePublisher = isContinueSetUpVisibleSubject.eraseToAnyPublisher() currentThemeName = .init(rawValue: persistor.currentThemeName) ?? .systemDefault showFullURL = persistor.showFullURL favoritesDisplayMode = persistor.favoritesDisplayMode.flatMap(FavoritesDisplayMode.init) ?? .default From cfef8b05334aa287c9a0f2ed55ddfba6d1b687a1 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 2 Dec 2024 15:03:25 +0100 Subject: [PATCH 04/62] Implement willDisplayCardsPublisher and send a pixel when addToDock is presented --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + .../NewTabPageNextStepsCardsClient.swift | 129 +++++++----------- .../NewTabPageNextStepsCardsProviding.swift | 115 ++++++++++++++++ 3 files changed, 167 insertions(+), 83 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 26a4c7fa7d..bc625234ab 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1107,6 +1107,8 @@ 372217822B33380700B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 372217812B33380700B8E9C2 /* TestUtils */; }; 3723A94F2CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; 3723A9502CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; + 3724ED8B2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */; }; + 3724ED8C2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */; }; 37269EFB2B332F9E005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFA2B332F9E005E8E46 /* Common */; }; 37269EFD2B332FAC005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFC2B332FAC005E8E46 /* Common */; }; 37269EFF2B332FBB005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFE2B332FBB005E8E46 /* Common */; }; @@ -3667,6 +3669,7 @@ 37219B392CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NewTabPageSearchBoxExperiment+Logger.swift"; sourceTree = ""; }; 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperimentTests.swift; sourceTree = ""; }; 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClient.swift; sourceTree = ""; }; + 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsProviding.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 = ""; }; @@ -6046,6 +6049,7 @@ 37CB36892CFA5ADA00E5E5FA /* NextStepsCards */ = { isa = PBXGroup; children = ( + 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */, 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */, ); path = NextStepsCards; @@ -11721,6 +11725,7 @@ 3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, B677FC552B064A9C0099EB04 /* DataImportViewModel.swift in Sources */, D64A5FF92AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, + 3724ED8B2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */, 4BE3A6C22C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */, 3707C717294B5D0F00682A9F /* FindInPageTabExtension.swift in Sources */, 3706FB81293F65D500E42796 /* IndexPathExtension.swift in Sources */, @@ -13114,6 +13119,7 @@ B6E3E5542BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, 3768D8382C24BFF5004120AE /* RemoteMessageView.swift in Sources */, 4BBDEE9428FC14760092FAA6 /* ConnectBitwardenViewController.swift in Sources */, + 3724ED8C2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */, 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */, 9833912F27AAA3CE00DAF119 /* AppTrackerDataSetProvider.swift in Sources */, B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */, diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index fbfba57037..6587cba5b9 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -16,84 +16,27 @@ // limitations under the License. // -import Bookmarks import Common import Combine import UserScript -protocol NewTabPageNextStepsCardsProviding: AnyObject { - var isViewExpanded: Bool { get set } - var isViewExpandedPublisher: AnyPublisher { get } - - var cards: [NewTabPageNextStepsCardsClient.CardID] { get } - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } - - @MainActor - func performAction(for card: NewTabPageNextStepsCardsClient.CardID) - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) -} - -extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding { - var isViewExpanded: Bool { - get { - shouldShowAllFeatures - } - set { - shouldShowAllFeatures = newValue - } - } - - var isViewExpandedPublisher: AnyPublisher { - $shouldShowAllFeatures.dropFirst().eraseToAnyPublisher() - } - - var cards: [NewTabPageNextStepsCardsClient.CardID] { - featuresMatrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } - } - - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { - $featuresMatrix.dropFirst() - .map { matrix in - matrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } - } - .eraseToAnyPublisher() - } - - @MainActor - func performAction(for card: NewTabPageNextStepsCardsClient.CardID) { - performAction(for: .init(card)) - } - - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { - removeItem(for: .init(card)) - } -} - -extension HomePage.Models.FeatureType { - init(_ card: NewTabPageNextStepsCardsClient.CardID) { - switch card { - case .bringStuff: - self = .importBookmarksAndPasswords - case .defaultApp: - self = .defaultBrowser - case .emailProtection: - self = .emailProtection - case .duckplayer: - self = .duckplayer - case .addAppToDockMac: - self = .dock - } - } -} - final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { let model: NewTabPageNextStepsCardsProviding + let willDisplayCardsPublisher: AnyPublisher<[CardID], Never> weak var userScriptsSource: NewTabPageUserScriptsSource? + + private let willDisplayCardsSubject = PassthroughSubject<[CardID], Never>() + private let getDataSubject = PassthroughSubject<[CardID], Never>() + private let getConfigSubject = PassthroughSubject() + private let notifyDataUpdatedSubject = PassthroughSubject<[CardID], Never>() + private let notifyConfigUpdatedSubject = PassthroughSubject() private var cancellables: Set = [] init(model: NewTabPageNextStepsCardsProviding) { self.model = model + willDisplayCardsPublisher = willDisplayCardsSubject.eraseToAnyPublisher() + connectWillDisplayCardsPublisher() model.cardsPublisher .sink { [weak self] cardIDs in @@ -112,6 +55,32 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { .store(in: &cancellables) } + private func connectWillDisplayCardsPublisher() { + let initialCards = Publishers.CombineLatest(getDataSubject, getConfigSubject) + .map { cards, isViewExpanded in + isViewExpanded ? cards : Array(cards.prefix(2)) + } + + // only notify about visible cards (i.e. if collapsed, only the first 2) + let cardsOnDataUpdated = notifyDataUpdatedSubject.map { [weak self] cards in + self?.model.isViewExpanded == true ? cards: Array(cards.prefix(2)) + } + + // only notify about cards revealed by expanding the view (i.e. other than the first 2) + let cardsOnConfigUpdated = notifyConfigUpdatedSubject.compactMap { [weak self] isViewExpanded -> [CardID]? in + guard let self, isViewExpanded, model.cards.count > 2 else { + return nil + } + return Array(self.model.cards.suffix(from: 2)) + } + + Publishers.Merge3(initialCards, cardsOnDataUpdated, cardsOnConfigUpdated) + .sink { [weak self] cards in + self?.willDisplayCardsSubject.send(cards) + } + .store(in: &cancellables) + } + enum MessageName: String, CaseIterable { case action = "nextSteps_action" case dismiss = "nextSteps_dismiss" @@ -150,6 +119,8 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = model.isViewExpanded ? .expanded : .collapsed + + getConfigSubject.send(model.isViewExpanded) return NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) } @@ -164,14 +135,19 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { @MainActor func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { - let cards = model.cards.map(Card.init(id:)) + let cardIDs = model.cards + let cards = cardIDs.map(Card.init(id:)) + + getDataSubject.send(cardIDs) return NextStepsData(content: cards.isEmpty ? nil : cards) } @MainActor - func notifyDataUpdated(_ cardIDs: [CardID]) { + private func notifyDataUpdated(_ cardIDs: [CardID]) { let cards = cardIDs.map(Card.init(id:)) let params = NextStepsData(content: cards.isEmpty ? nil : cards) + + notifyDataUpdatedSubject.send(cardIDs) pushMessage(named: MessageName.onDataUpdate.rawValue, params: params) } @@ -179,6 +155,8 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { private func notifyConfigUpdated(_ showAllCards: Bool) { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = showAllCards ? .expanded : .collapsed let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) + + notifyConfigUpdatedSubject.send(showAllCards) pushMessage(named: MessageName.onConfigUpdate.rawValue, params: config) } } @@ -191,21 +169,6 @@ extension NewTabPageNextStepsCardsClient { case emailProtection case duckplayer case addAppToDockMac - - init(_ feature: HomePage.Models.FeatureType) { - switch feature { - case .duckplayer: - self = .duckplayer - case .emailProtection: - self = .emailProtection - case .defaultBrowser: - self = .defaultApp - case .dock: - self = .addAppToDockMac - case .importBookmarksAndPasswords: - self = .bringStuff - } - } } struct Card: Codable { diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift new file mode 100644 index 0000000000..f663ea6d64 --- /dev/null +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -0,0 +1,115 @@ +// +// NewTabPageNextStepsCardsProviding.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 Common +import Combine +import UserScript +import PixelKit + +protocol NewTabPageNextStepsCardsProviding: AnyObject { + var isViewExpanded: Bool { get set } + var isViewExpandedPublisher: AnyPublisher { get } + + var cards: [NewTabPageNextStepsCardsClient.CardID] { get } + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } + + @MainActor + func performAction(for card: NewTabPageNextStepsCardsClient.CardID) + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) + + func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) +} + +extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding { + var isViewExpanded: Bool { + get { + shouldShowAllFeatures + } + set { + shouldShowAllFeatures = newValue + } + } + + var isViewExpandedPublisher: AnyPublisher { + $shouldShowAllFeatures.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + var cards: [NewTabPageNextStepsCardsClient.CardID] { + featuresMatrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } + } + + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { + $featuresMatrix.dropFirst().removeDuplicates() + .map { matrix in + matrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } + } + .eraseToAnyPublisher() + } + + @MainActor + func performAction(for card: NewTabPageNextStepsCardsClient.CardID) { + performAction(for: .init(card)) + } + + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { + removeItem(for: .init(card)) + } + + func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { + guard cards.contains(.addAppToDockMac) else { + return + } + PixelKit.fire(GeneralPixel.addToDockNewTabPageCardPresented, + frequency: .unique, + includeAppVersionParameter: false) + } +} + +extension HomePage.Models.FeatureType { + init(_ card: NewTabPageNextStepsCardsClient.CardID) { + switch card { + case .bringStuff: + self = .importBookmarksAndPasswords + case .defaultApp: + self = .defaultBrowser + case .emailProtection: + self = .emailProtection + case .duckplayer: + self = .duckplayer + case .addAppToDockMac: + self = .dock + } + } +} + +extension NewTabPageNextStepsCardsClient.CardID { + init(_ feature: HomePage.Models.FeatureType) { + switch feature { + case .duckplayer: + self = .duckplayer + case .emailProtection: + self = .emailProtection + case .defaultBrowser: + self = .defaultApp + case .dock: + self = .addAppToDockMac + case .importBookmarksAndPasswords: + self = .bringStuff + } + } +} From bde24fb4003ca2caef1b1d6179a7994f5281fc62 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 2 Dec 2024 23:59:02 +0100 Subject: [PATCH 05/62] Add NewTabPageNextStepsCardsClientTests with basic tests --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + .../NewTabPageNextStepsCardsClient.swift | 6 +- .../NewTabPageNextStepsCardsProviding.swift | 4 +- .../HomePage/ContinueSetUpModelTests.swift | 12 +- .../NewTabPageNextStepsCardsClientTests.swift | 153 ++++++++++++++++++ 5 files changed, 170 insertions(+), 11 deletions(-) create mode 100644 UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index bc625234ab..d350d104a2 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1109,6 +1109,8 @@ 3723A9502CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; 3724ED8B2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */; }; 3724ED8C2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */; }; + 3724ED8E2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */; }; + 3724ED8F2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */; }; 37269EFB2B332F9E005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFA2B332F9E005E8E46 /* Common */; }; 37269EFD2B332FAC005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFC2B332FAC005E8E46 /* Common */; }; 37269EFF2B332FBB005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFE2B332FBB005E8E46 /* Common */; }; @@ -3670,6 +3672,7 @@ 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperimentTests.swift; sourceTree = ""; }; 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClient.swift; sourceTree = ""; }; 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsProviding.swift; sourceTree = ""; }; + 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsClientTests.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 = ""; }; @@ -5882,6 +5885,7 @@ 3767880E2CECD5A200F59D83 /* NewTabPageUserScriptTests.swift */, 3767880B2CECCB6C00F59D83 /* NewTabPageActionsManagerTests.swift */, 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */, + 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */, 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */, 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */, 37FC2A1A2CF903A70048E226 /* NewTabPagePrivacyStatsClientTests.swift */, @@ -12614,6 +12618,7 @@ CD33012A2C887B1C009AA127 /* URLTokenValidatorTests.swift in Sources */, 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, + 3724ED8F2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */, 56A214B02CB583BF00E5BC0E /* TrackerMessageProviderTests.swift in Sources */, 3706FE82293F661700E42796 /* MockStatisticsStore.swift in Sources */, 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, @@ -14137,6 +14142,7 @@ 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 56A054302C2043C8007D8FAB /* OnboardingTabExtensionTests.swift in Sources */, + 3724ED8E2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */, BDCB66D82C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */, 567A23E12C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index 6587cba5b9..8840ab6e22 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -105,7 +105,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { guard let card: NewTabPageNextStepsCardsClient.Card = DecodableHelper.decode(from: params) else { return nil } - await model.performAction(for: card.id) + await model.handleAction(for: card.id) return nil } @@ -171,11 +171,11 @@ extension NewTabPageNextStepsCardsClient { case addAppToDockMac } - struct Card: Codable { + struct Card: Codable, Equatable { let id: CardID } - struct NextStepsData: Codable { + struct NextStepsData: Codable, Equatable { let content: [Card]? } } diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift index f663ea6d64..386ac832f2 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -29,7 +29,7 @@ protocol NewTabPageNextStepsCardsProviding: AnyObject { var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } @MainActor - func performAction(for card: NewTabPageNextStepsCardsClient.CardID) + func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) @@ -62,7 +62,7 @@ extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding } @MainActor - func performAction(for card: NewTabPageNextStepsCardsClient.CardID) { + func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) { performAction(for: .init(card)) } diff --git a/UnitTests/HomePage/ContinueSetUpModelTests.swift b/UnitTests/HomePage/ContinueSetUpModelTests.swift index 8e9393accd..cc3ff92925 100644 --- a/UnitTests/HomePage/ContinueSetUpModelTests.swift +++ b/UnitTests/HomePage/ContinueSetUpModelTests.swift @@ -53,7 +53,7 @@ final class ContinueSetUpModelTests: XCTestCase { defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, - tabCollectionViewModel: tabCollectionVM, + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: tabCollectionVM), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, privacyConfigurationManager: privacyConfigManager @@ -95,7 +95,7 @@ final class ContinueSetUpModelTests: XCTestCase { defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, - tabCollectionViewModel: tabCollectionVM, + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: tabCollectionVM), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences ) @@ -111,7 +111,7 @@ final class ContinueSetUpModelTests: XCTestCase { defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, - tabCollectionViewModel: tabCollectionVM, + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: tabCollectionVM), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences ) @@ -315,7 +315,7 @@ final class ContinueSetUpModelTests: XCTestCase { defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, - tabCollectionViewModel: tabCollectionVM, + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: tabCollectionVM), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences ) @@ -330,7 +330,7 @@ final class ContinueSetUpModelTests: XCTestCase { defaultBrowserProvider: capturingDefaultBrowserProvider, dockCustomizer: dockCustomizer, dataImportProvider: capturingDataImportProvider, - tabCollectionViewModel: tabCollectionVM, + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: tabCollectionVM), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences ) @@ -427,7 +427,7 @@ extension HomePage.Models.ContinueSetUpModel { defaultBrowserProvider: defaultBrowserProvider, dockCustomizer: dockCustomizer, dataImportProvider: dataImportProvider, - tabCollectionViewModel: TabCollectionViewModel(), + tabOpener: TabCollectionViewModelTabOpener(tabCollectionViewModel: TabCollectionViewModel()), emailManager: emailManager, duckPlayerPreferences: duckPlayerPreferences, privacyConfigurationManager: manager) diff --git a/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift b/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift new file mode 100644 index 0000000000..c5f5b84116 --- /dev/null +++ b/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift @@ -0,0 +1,153 @@ +// +// NewTabPageNextStepsCardsClientTests.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 TestUtils +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class CapturingNewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding { + + @Published var isViewExpanded: Bool = false + var isViewExpandedPublisher: AnyPublisher { + $isViewExpanded.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + @Published var cards: [NewTabPageNextStepsCardsClient.CardID] = [] + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { + $cards.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) { + handleActionCalls.append(card) + } + + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { + dismissCalls.append(card) + } + + func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { + willDisplayCardsCalls.append(cards) + } + + var handleActionCalls: [NewTabPageNextStepsCardsClient.CardID] = [] + var dismissCalls: [NewTabPageNextStepsCardsClient.CardID] = [] + var willDisplayCardsCalls: [[NewTabPageNextStepsCardsClient.CardID]] = [] +} + +final class NewTabPageNextStepsCardsClientTests: XCTestCase { + var client: NewTabPageNextStepsCardsClient! + var model: CapturingNewTabPageNextStepsCardsProvider! + var userScript: NewTabPageUserScript! + + @MainActor + override func setUpWithError() throws { + try super.setUpWithError() + model = CapturingNewTabPageNextStepsCardsProvider() + client = NewTabPageNextStepsCardsClient(model: model) + + userScript = NewTabPageUserScript() + client.registerMessageHandlers(for: userScript) + } + + // MARK: - action + + func testThatActionCallsHandleAction() async throws { + try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageNextStepsCardsClient.Card(id: .defaultApp)) + try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageNextStepsCardsClient.Card(id: .duckplayer)) + try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageNextStepsCardsClient.Card(id: .bringStuff)) + XCTAssertEqual(model.handleActionCalls, [.defaultApp, .duckplayer, .bringStuff]) + } + + // MARK: - dismiss + + func testThatDismissCallsDismissHandler() async throws { + try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageNextStepsCardsClient.Card(id: .defaultApp)) + try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageNextStepsCardsClient.Card(id: .duckplayer)) + try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageNextStepsCardsClient.Card(id: .bringStuff)) + XCTAssertEqual(model.dismissCalls, [.defaultApp, .duckplayer, .bringStuff]) + } + + // MARK: - getConfig + + func testWhenNextStepsViewIsExpandedThenGetConfigReturnsExpandedState() async throws { + model.isViewExpanded = true + let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + XCTAssertEqual(config.animation, .auto) + XCTAssertEqual(config.expansion, .expanded) + } + + func testWhenNextStepsViewIsCollapsedThenGetConfigReturnsCollapsedState() async throws { + model.isViewExpanded = false + let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + XCTAssertEqual(config.animation, .auto) + XCTAssertEqual(config.expansion, .collapsed) + } + + // MARK: - setConfig + + func testWhenSetConfigContainsExpandedStateThenModelSettingIsSetToExpanded() async throws { + model.isViewExpanded = false + let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .expanded) + try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + XCTAssertEqual(model.isViewExpanded, true) + } + + func testWhenSetConfigContainsCollapsedStateThenModelSettingIsSetToCollapsed() async throws { + model.isViewExpanded = true + let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .collapsed) + try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + XCTAssertEqual(model.isViewExpanded, false) + } + + // MARK: - getData + + func testThatGetDataReturnsCardsFromTheModel() async throws { + model.cards = [ + .addAppToDockMac, + .duckplayer, + .bringStuff + ] + let data: NewTabPageNextStepsCardsClient.NextStepsData = try await handleMessage(named: .getData) + XCTAssertEqual(data, .init(content: [ + .init(id: .addAppToDockMac), + .init(id: .duckplayer), + .init(id: .bringStuff) + ])) + } + + func testWhenCardsAreEmptyThenGetDataReturnsNilContent() async throws { + model.cards = [] + let data: NewTabPageNextStepsCardsClient.NextStepsData = try await handleMessage(named: .getData) + XCTAssertEqual(data, .init(content: nil)) + } + + // MARK: - Helper functions + + func handleMessage(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + return try XCTUnwrap(response as? Response, file: file, line: line) + } + + func handleMessageExpectingNilResponse(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + XCTAssertNil(response, file: file, line: line) + } +} From bcefaa55500cd927e69a8ec1ef97ced8ab5d0f7b Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 3 Dec 2024 10:16:40 +0100 Subject: [PATCH 06/62] Add tests for willDisplayCardsPublisher --- .../NewTabPageNextStepsCardsClient.swift | 30 +++- .../NewTabPageNextStepsCardsClientTests.swift | 141 ++++++++++++++++++ 2 files changed, 163 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index 8840ab6e22..ecf71796b6 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -53,6 +53,12 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } } .store(in: &cancellables) + + willDisplayCardsPublisher + .sink { cards in + model.willDisplayCards(cards) + } + .store(in: &cancellables) } private func connectWillDisplayCardsPublisher() { @@ -60,21 +66,29 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { .map { cards, isViewExpanded in isViewExpanded ? cards : Array(cards.prefix(2)) } + .share() + + let firstInitialCards = initialCards.first() // only notify about visible cards (i.e. if collapsed, only the first 2) - let cardsOnDataUpdated = notifyDataUpdatedSubject.map { [weak self] cards in - self?.model.isViewExpanded == true ? cards: Array(cards.prefix(2)) - } + let cardsOnDataUpdated = notifyDataUpdatedSubject + .drop(untilOutputFrom: firstInitialCards) + .map { [weak self] cards in + self?.model.isViewExpanded == true ? cards: Array(cards.prefix(2)) + } // only notify about cards revealed by expanding the view (i.e. other than the first 2) - let cardsOnConfigUpdated = notifyConfigUpdatedSubject.compactMap { [weak self] isViewExpanded -> [CardID]? in - guard let self, isViewExpanded, model.cards.count > 2 else { - return nil + let cardsOnConfigUpdated = notifyConfigUpdatedSubject + .drop(untilOutputFrom: firstInitialCards) + .compactMap { [weak self] isViewExpanded -> [CardID]? in + guard let self, isViewExpanded, model.cards.count > 2 else { + return nil + } + return Array(self.model.cards.suffix(from: 2)) } - return Array(self.model.cards.suffix(from: 2)) - } Publishers.Merge3(initialCards, cardsOnDataUpdated, cardsOnConfigUpdated) + .filter { !$0.isEmpty } .sink { [weak self] cards in self?.willDisplayCardsSubject.send(cards) } diff --git a/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift b/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift index c5f5b84116..a8ee7acec4 100644 --- a/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift @@ -43,11 +43,13 @@ final class CapturingNewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsP func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { willDisplayCardsCalls.append(cards) + willDisplayCardsImpl?(cards) } var handleActionCalls: [NewTabPageNextStepsCardsClient.CardID] = [] var dismissCalls: [NewTabPageNextStepsCardsClient.CardID] = [] var willDisplayCardsCalls: [[NewTabPageNextStepsCardsClient.CardID]] = [] + var willDisplayCardsImpl: (([NewTabPageNextStepsCardsClient.CardID]) -> Void)? } final class NewTabPageNextStepsCardsClientTests: XCTestCase { @@ -137,8 +139,147 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { XCTAssertEqual(data, .init(content: nil)) } + // MARK: - willDisplayCardsPublisher + + func testThatWillDisplayCardsPublisherIsSentAfterGetDataAndGetConfigAreCalled() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + _ = try await client.getData(params: [], original: .init()) + _ = try await client.getConfig(params: [], original: .init()) + + XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) + } + + func testThatWillDisplayCardsPublisherIsNotSentBeforeGetConfigIsCalled() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + _ = try await client.getData(params: [], original: .init()) + _ = try await client.getData(params: [], original: .init()) + _ = try await client.getData(params: [], original: .init()) + _ = try await client.getData(params: [], original: .init()) + _ = try await client.getData(params: [], original: .init()) + + XCTAssertEqual(model.willDisplayCardsCalls, []) + + _ = try await client.getConfig(params: [], original: .init()) + + XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) + } + + func testThatWillDisplayCardsPublisherIsNotSentBeforeGetDataIsCalled() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + _ = try await client.getConfig(params: [], original: .init()) + _ = try await client.getConfig(params: [], original: .init()) + _ = try await client.getConfig(params: [], original: .init()) + _ = try await client.getConfig(params: [], original: .init()) + _ = try await client.getConfig(params: [], original: .init()) + + XCTAssertEqual(model.willDisplayCardsCalls, []) + + _ = try await client.getData(params: [], original: .init()) + + XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) + } + + func testThatWillDisplayCardsPublisherIsSentAfterUpdatingCards() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + model.isViewExpanded = true + try await triggerInitialCardsEventAndResetMockState() + + let expectation = self.expectation(description: "willDisplayCards") + model.willDisplayCardsImpl = { _ in expectation.fulfill() } + + model.cards = [.addAppToDockMac, .duckplayer, .bringStuff] + await fulfillment(of: [expectation], timeout: 0.1) + XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer, .bringStuff]]) + } + + func testWhenCardsAreUpdatedThenWillDisplayCardsEventOnlyContainsCurrentlyVisibleCards() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + model.isViewExpanded = false + try await triggerInitialCardsEventAndResetMockState() + + let expectation = self.expectation(description: "willDisplayCards") + expectation.expectedFulfillmentCount = 3 + model.willDisplayCardsImpl = { _ in expectation.fulfill() } + + model.cards = [.addAppToDockMac, .duckplayer, .bringStuff] + model.cards = [.duckplayer, .addAppToDockMac, .bringStuff] + model.cards = [.addAppToDockMac, .emailProtection, .duckplayer] + await fulfillment(of: [expectation], timeout: 0.1) + XCTAssertEqual(model.willDisplayCardsCalls, [ + [.addAppToDockMac, .duckplayer], + [.duckplayer, .addAppToDockMac], + [.addAppToDockMac, .emailProtection] + ]) + } + + func testThatWillDisplayCardsEventIsNotPublishedWhenCardsIsEmpty() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + model.isViewExpanded = false + try await triggerInitialCardsEventAndResetMockState() + + let expectation = self.expectation(description: "willDisplayCards") + expectation.expectedFulfillmentCount = 2 + model.willDisplayCardsImpl = { _ in expectation.fulfill() } + + model.cards = [.addAppToDockMac, .duckplayer, .bringStuff] + model.cards = [] + model.cards = [.addAppToDockMac, .emailProtection, .duckplayer] + await fulfillment(of: [expectation], timeout: 0.1) + XCTAssertEqual(model.willDisplayCardsCalls, [ + [.addAppToDockMac, .duckplayer], + [.addAppToDockMac, .emailProtection] + ]) + } + + func testThatWillDisplayCardsPublisherIsSentAfterExpandingViewToRevealMoreCards() async throws { + model.cards = [.addAppToDockMac, .duckplayer, .emailProtection, .bringStuff, .defaultApp] + model.isViewExpanded = false + try await triggerInitialCardsEventAndResetMockState() + + let expectation = self.expectation(description: "willDisplayCards") + model.willDisplayCardsImpl = { _ in expectation.fulfill() } + + model.isViewExpanded = true + await fulfillment(of: [expectation], timeout: 0.1) + XCTAssertEqual(model.willDisplayCardsCalls, [[.emailProtection, .bringStuff, .defaultApp]]) + } + + func testThatWillDisplayCardsPublisherIsSentAfterExpandingViewAndNotRevealingMoreCards() async throws { + model.cards = [.addAppToDockMac, .duckplayer] + model.isViewExpanded = false + try await triggerInitialCardsEventAndResetMockState() + + let expectation = self.expectation(description: "willDisplayCards") + expectation.isInverted = true + model.willDisplayCardsImpl = { _ in expectation.fulfill() } + + model.isViewExpanded = true + await fulfillment(of: [expectation], timeout: 0.1) + XCTAssertEqual(model.willDisplayCardsCalls, []) + } + + func testThatWillDisplayCardsPublisherIsNotSentAfterCollapsingView() async throws { + model.cards = [.addAppToDockMac, .duckplayer, .emailProtection] + model.isViewExpanded = true + try await triggerInitialCardsEventAndResetMockState() + + let expectation = self.expectation(description: "willDisplayCards") + expectation.isInverted = true + model.willDisplayCardsImpl = { _ in expectation.fulfill() } + + model.isViewExpanded = false + await fulfillment(of: [expectation], timeout: 0.1) + XCTAssertEqual(model.willDisplayCardsCalls, []) + } + // MARK: - Helper functions + func triggerInitialCardsEventAndResetMockState() async throws { + _ = try await client.getConfig(params: [], original: .init()) + _ = try await client.getData(params: [], original: .init()) + model.willDisplayCardsCalls = [] + } + func handleMessage(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) From bd67285a5ad6732dbce3ce55138fa692e8b4e33e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 3 Dec 2024 13:21:44 +0100 Subject: [PATCH 07/62] Fix ContinueSetUpCardsTests --- .../Model/HomePageContinueSetUpModel.swift | 14 ++++++++++---- .../NewTabPageNextStepsCardsProviding.swift | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index 16c96f0671..c1a03d2fbd 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -18,6 +18,7 @@ import AppKit import BrowserServicesKit +import Combine import Common import Foundation import PixelKit @@ -68,15 +69,18 @@ extension HomePage.Models { private let duckPlayerPreferences: DuckPlayerPreferencesPersistor private let subscriptionManager: SubscriptionManager - @Published - var shouldShowAllFeatures: Bool = UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false).wrappedValue { + + @UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) + var shouldShowAllFeatures: Bool { didSet { - let udWrapper = UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) - udWrapper.wrappedValue = shouldShowAllFeatures updateVisibleMatrix() + shouldShowAllFeaturesSubject.send(shouldShowAllFeatures) } } + let shouldShowAllFeaturesPublisher: AnyPublisher + private let shouldShowAllFeaturesSubject = PassthroughSubject() + struct Settings { @UserDefaultsWrapper(key: .homePageShowMakeDefault, defaultValue: true) var shouldShowMakeDefaultSetting: Bool @@ -145,6 +149,8 @@ extension HomePage.Models { self.subscriptionManager = subscriptionManager self.settings = .init() + shouldShowAllFeaturesPublisher = shouldShowAllFeaturesSubject.removeDuplicates().eraseToAnyPublisher() + refreshFeaturesMatrix() NotificationCenter.default.addObserver(self, selector: #selector(newTabOpenNotification(_:)), name: HomePage.Models.newHomePageTabOpen, object: nil) diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift index 386ac832f2..c6daa96461 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -46,7 +46,7 @@ extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding } var isViewExpandedPublisher: AnyPublisher { - $shouldShowAllFeatures.dropFirst().removeDuplicates().eraseToAnyPublisher() + shouldShowAllFeaturesPublisher.eraseToAnyPublisher() } var cards: [NewTabPageNextStepsCardsClient.CardID] { From 023ea19c278d4c2b761ca02d23c7033c0655371f Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 3 Dec 2024 13:22:02 +0100 Subject: [PATCH 08/62] Fix NewTabPageConfigurationClientTests --- UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift b/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift index 9b115de48b..3659c80511 100644 --- a/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift @@ -88,6 +88,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { let configuration: NewTabPageUserScript.NewTabPageConfiguration = try await sendMessage(named: .initialSetup) XCTAssertEqual(configuration.widgets, [ .init(id: .rmf), + .init(id: .nextSteps), .init(id: .favorites), .init(id: .privacyStats) ]) From 3dd6e23960116b8ddc9d1ef4120113c389a211e4 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 3 Dec 2024 13:22:24 +0100 Subject: [PATCH 09/62] Hide Next Steps customization option from Appearance settings when HTML NTP is available --- .../Preferences/Model/AppearancePreferences.swift | 14 ++++++++++++-- .../View/PreferencesAppearanceView.swift | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index 43fc227272..8f87ad4e1c 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -16,10 +16,12 @@ // limitations under the License. // -import Foundation import AppKit import Bookmarks +import BrowserServicesKit import Common +import FeatureFlags +import Foundation import PixelKit import os.log @@ -165,7 +167,7 @@ enum ThemeName: String, Equatable, CaseIterable { } } -extension FavoritesDisplayMode: LosslessStringConvertible { +extension FavoritesDisplayMode: @retroactive LosslessStringConvertible { static let `default` = FavoritesDisplayMode.displayNative(.desktop) public init?(_ description: String) { @@ -232,6 +234,11 @@ final class AppearancePreferences: ObservableObject { } } + var isContinueSetUpCardsVisibilityControlAvailable: Bool { + // HTML NTP doesn't allow for hiding Next Steps Cards section + !featureFlagger().isFeatureOn(.htmlNewTabPage) + } + var isContinueSetUpVisible: Bool { get { return persistor.isContinueSetUpVisible && !persistor.continueSetUpCardsClosed && !isContinueSetUpCardsViewOutdated @@ -337,12 +344,14 @@ final class AppearancePreferences: ObservableObject { init( persistor: AppearancePreferencesPersistor = AppearancePreferencesUserDefaultsPersistor(), homePageNavigator: HomePageNavigator = DefaultHomePageNavigator(), + featureFlagger: @autoclosure @escaping () -> FeatureFlagger = NSApp.delegateTyped.featureFlagger, dateTimeProvider: @escaping () -> Date = Date.init ) { self.persistor = persistor self.homePageNavigator = homePageNavigator self.dateTimeProvider = dateTimeProvider self.isContinueSetUpCardsViewOutdated = persistor.continueSetUpCardsNumberOfDaysDemonstrated >= Constants.dismissNextStepsCardsAfterDays + self.featureFlagger = featureFlagger self.continueSetUpCardsClosed = persistor.continueSetUpCardsClosed currentThemeName = .init(rawValue: persistor.currentThemeName) ?? .systemDefault showFullURL = persistor.showFullURL @@ -359,6 +368,7 @@ final class AppearancePreferences: ObservableObject { private var persistor: AppearancePreferencesPersistor private var homePageNavigator: HomePageNavigator + private let featureFlagger: () -> FeatureFlagger private let dateTimeProvider: () -> Date private func requestSync() { diff --git a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift index 606e3955ab..d8e77d151b 100644 --- a/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift +++ b/DuckDuckGo/Preferences/View/PreferencesAppearanceView.swift @@ -108,7 +108,7 @@ extension Preferences { if addressBarModel.shouldShowAddressBar { ToggleMenuItem(UserText.newTabSearchBarSectionTitle, isOn: $model.isSearchBarVisible) } - if model.isContinueSetUpAvailable && !model.isContinueSetUpCardsViewOutdated && !model.continueSetUpCardsClosed { + if model.isContinueSetUpCardsVisibilityControlAvailable && model.isContinueSetUpAvailable && !model.isContinueSetUpCardsViewOutdated && !model.continueSetUpCardsClosed { ToggleMenuItem(UserText.newTabSetUpSectionTitle, isOn: $model.isContinueSetUpVisible) } ToggleMenuItem(UserText.newTabFavoriteSectionTitle, isOn: $model.isFavoriteVisible).accessibilityIdentifier("Preferences.AppearanceView.showFavoritesToggle") From 1c207c68b0ce25ebe47905f92641580069e70650 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 3 Dec 2024 15:06:02 +0100 Subject: [PATCH 10/62] Add newTabPageWebViewDidAppear notification --- .../HomePage/Model/HomePageContinueSetUpModel.swift | 9 ++++++++- DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift | 11 ++++++++++- .../NewTabPageNextStepsCardsClient.swift | 4 +++- .../NewTabPageNextStepsCardsProviding.swift | 3 +++ 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index c1a03d2fbd..59881ea40b 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -69,7 +69,6 @@ extension HomePage.Models { private let duckPlayerPreferences: DuckPlayerPreferencesPersistor private let subscriptionManager: SubscriptionManager - @UserDefaultsWrapper(key: .homePageShowAllFeatures, defaultValue: false) var shouldShowAllFeatures: Bool { didSet { @@ -155,6 +154,10 @@ extension HomePage.Models { NotificationCenter.default.addObserver(self, selector: #selector(newTabOpenNotification(_:)), name: HomePage.Models.newHomePageTabOpen, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeKey(_:)), name: NSWindow.didBecomeKeyNotification, object: nil) + + // HTML NTP doesn't refresh on appear so we have to connect to the appear signal + // (the notification in this case) to trigger a refresh. + NotificationCenter.default.addObserver(self, selector: #selector(refreshFeaturesForHTMLNewTabPage(_:)), name: .newTabPageWebViewDidAppear, object: nil) } @MainActor func performAction(for featureType: FeatureType) { @@ -269,6 +272,10 @@ extension HomePage.Models { refreshFeaturesMatrix() } + @objc private func refreshFeaturesForHTMLNewTabPage(_ notification: Notification) { + refreshFeaturesMatrix() + } + var randomisedFeatures: [FeatureType] { var features: [FeatureType] = [.defaultBrowser] var shuffledFeatures = FeatureType.allCases.filter { $0 != .defaultBrowser } diff --git a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift index 73f368ca0a..0d3fcb3294 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift @@ -52,7 +52,12 @@ final class NewTabPageWebViewModel: NSObject { windowCancellable = webView.publisher(for: \.window) .map { $0 != nil } - .assign(to: \.isViewOnScreen, on: activeRemoteMessageModel) + .sink { [weak activeRemoteMessageModel] isOnScreen in + activeRemoteMessageModel?.isViewOnScreen = isOnScreen + if isOnScreen { + NotificationCenter.default.post(name: .newTabPageWebViewDidAppear, object: nil) + } + } } } @@ -61,3 +66,7 @@ extension NewTabPageWebViewModel: WKNavigationDelegate { navigationAction.request.url == .newtab ? .allow : .cancel } } + +extension Notification.Name { + static var newTabPageWebViewDidAppear = Notification.Name("newTabPageWebViewDidAppear") +} diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index ecf71796b6..f377d1dbde 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -115,14 +115,16 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { ]) } + @MainActor func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let card: NewTabPageNextStepsCardsClient.Card = DecodableHelper.decode(from: params) else { return nil } - await model.handleAction(for: card.id) + model.handleAction(for: card.id) return nil } + @MainActor func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let card: Card = DecodableHelper.decode(from: params) else { return nil diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift index c6daa96461..d508f01d2c 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -30,6 +30,8 @@ protocol NewTabPageNextStepsCardsProviding: AnyObject { @MainActor func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) + + @MainActor func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) @@ -66,6 +68,7 @@ extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding performAction(for: .init(card)) } + @MainActor func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { removeItem(for: .init(card)) } From 1e5eafebb576562ed3a981d836bce9ad7bdccf85 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 4 Dec 2024 20:36:42 +0100 Subject: [PATCH 11/62] Fix hiding default browser card after actioning the system dialog --- DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift index 59881ea40b..47c5ba9665 100644 --- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift @@ -269,7 +269,11 @@ extension HomePage.Models { } @objc private func windowDidBecomeKey(_ notification: Notification) { - refreshFeaturesMatrix() + // Async dispatch allows default browser setting to propagate + // after being changed in the system dialog + DispatchQueue.main.async { + self.refreshFeaturesMatrix() + } } @objc private func refreshFeaturesForHTMLNewTabPage(_ notification: Notification) { From a30c8ffbf2dfb7088bb7b8400b3055706e7c1866 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 4 Dec 2024 14:32:05 +0100 Subject: [PATCH 12/62] Add NewTabPage and WebKitExtensions local packages --- DuckDuckGo.xcodeproj/project.pbxproj | 80 ++++++++++--------- DuckDuckGo/Application/AppDelegate.swift | 1 + .../NewTabPageFavoritesActionsHandler.swift | 1 + .../Favorites/NewTabPageFavoritesClient.swift | 1 + .../Favorites/NewTabPageFavoritesModel.swift | 1 + .../NewTabPageActionsManagerExtension.swift | 51 ++++++++++++ .../NewTabPageConfigurationClient.swift | 1 + .../NewTabPage/NewTabPageRMFClient.swift | 1 + .../NewTabPage/NewTabPageWebViewModel.swift | 16 ++++ .../NewTabPageNextStepsCardsClient.swift | 1 + .../NewTabPageNextStepsCardsProviding.swift | 3 +- .../NewTabPagePrivacyStatsClient.swift | 1 + .../NewTabPagePrivacyStatsModel.swift | 1 + .../Tab/View/BrowserTabViewController.swift | 13 +-- LocalPackages/NewTabPage/.gitignore | 8 ++ LocalPackages/NewTabPage/Package.swift | 52 ++++++++++++ .../NewTabPage/NewTabPageActionsManager.swift | 49 ++---------- .../NewTabPageContextMenuPresenting.swift | 8 +- .../NewTabPageUserContentController.swift | 32 +++----- .../NewTabPage/NewTabPageUserScript.swift | 51 +++++++----- LocalPackages/WebKitExtensions/.gitignore | 8 ++ LocalPackages/WebKitExtensions/Package.swift | 48 +++++++++++ .../WKUserContentControllerExtension.swift | 2 +- .../WebKitExtensionsTests.swift | 6 ++ 24 files changed, 304 insertions(+), 132 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift create mode 100644 LocalPackages/NewTabPage/.gitignore create mode 100644 LocalPackages/NewTabPage/Package.swift rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/NewTabPageActionsManager.swift (66%) rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/NewTabPageContextMenuPresenting.swift (80%) rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/NewTabPageUserContentController.swift (64%) rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/NewTabPageUserScript.swift (56%) create mode 100644 LocalPackages/WebKitExtensions/.gitignore create mode 100644 LocalPackages/WebKitExtensions/Package.swift rename {DuckDuckGo/Common/Extensions => LocalPackages/WebKitExtensions/Sources/WebKitExtensions}/WKUserContentControllerExtension.swift (97%) create mode 100644 LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4fe5ed36af..42dfac7c0b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1042,7 +1042,6 @@ 3707C71B294B5D0F00682A9F /* AutofillTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ECA292F839D009C73A6 /* AutofillTabExtension.swift */; }; 3707C71C294B5D1900682A9F /* TabExtensionsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ED6292FB4B4009C73A6 /* TabExtensionsBuilder.swift */; }; 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA88D14A252A557100980B4E /* URLRequestExtension.swift */; }; - 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */; }; 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */; }; 3707C721294B5D2900682A9F /* WKMenuItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */; }; 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA92127625ADA07900600CD4 /* WKWebViewExtension.swift */; }; @@ -1093,6 +1092,12 @@ 37197EAA2942443D00394917 /* ModalSheetCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6BE9FA9293F7955006363C6 /* ModalSheetCancellable.swift */; }; 37197EAB2942443D00394917 /* WebViewContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B2400D28083B49001B8F3A /* WebViewContainerView.swift */; }; 37197EAC294244D600394917 /* FutureExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B634DBE6293C98C500C3C99E /* FutureExtension.swift */; }; + 371BBC572D00C891008FA0C7 /* NewTabPage in Frameworks */ = {isa = PBXBuildFile; productRef = 371BBC562D00C891008FA0C7 /* NewTabPage */; }; + 371BBC592D00C897008FA0C7 /* NewTabPage in Frameworks */ = {isa = PBXBuildFile; productRef = 371BBC582D00C897008FA0C7 /* NewTabPage */; }; + 371BBC5B2D00C919008FA0C7 /* NewTabPageActionsManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */; }; + 371BBC5C2D00C919008FA0C7 /* NewTabPageActionsManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */; }; + 371BBC5E2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */; }; + 371BBC5F2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */; }; 371C0A2927E33EDC0070591F /* FeedbackPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371C0A2827E33EDC0070591F /* FeedbackPresenter.swift */; }; 371D00E129D8509400EC8598 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 371D00E029D8509400EC8598 /* OpenSSL */; }; 37219B342CBFBBE800C9D7A8 /* NewTabPageSearchBoxExperiment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37219B332CBFBBDB00C9D7A8 /* NewTabPageSearchBoxExperiment.swift */; }; @@ -1120,8 +1125,6 @@ 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 */; }; @@ -1147,8 +1150,6 @@ 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 /* 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 */; }; @@ -1309,6 +1310,8 @@ 37E260902C8A3ABE006EE07F /* HomePageSettingsModelNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E2608E2C8A3ABE006EE07F /* HomePageSettingsModelNavigator.swift */; }; 37E260922C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E260912C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift */; }; 37E260932C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E260912C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift */; }; + 37EE81412D00DBC40068034A /* WebKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 37EE81402D00DBC40068034A /* WebKitExtensions */; }; + 37EE81432D00DBCC0068034A /* WebKitExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 37EE81422D00DBCC0068034A /* WebKitExtensions */; }; 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */; }; 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */; }; @@ -1319,14 +1322,8 @@ 37F44A5F298C17830025E7FE /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 37F44A5E298C17830025E7FE /* Navigation */; }; 37F8ABD32CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; 37F8ABD42CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; - 37F8E2332CEE3646002F0141 /* NewTabPageContextMenuPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8E2322CEE363D002F0141 /* NewTabPageContextMenuPresenting.swift */; }; - 37F8E2342CEE3646002F0141 /* NewTabPageContextMenuPresenting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8E2322CEE363D002F0141 /* NewTabPageContextMenuPresenting.swift */; }; 37F8E2362CEE3C01002F0141 /* NewTabPageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */; }; 37F8E2372CEE3C01002F0141 /* NewTabPageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */; }; - 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 */; }; 37FC2A0E2CF8DFA20048E226 /* NewTabPagePrivacyStatsModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A0D2CF8DFA00048E226 /* NewTabPagePrivacyStatsModelTests.swift */; }; 37FC2A0F2CF8DFA20048E226 /* NewTabPagePrivacyStatsModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A0D2CF8DFA00048E226 /* NewTabPagePrivacyStatsModelTests.swift */; }; 37FC2A182CF903080048E226 /* MockPrivacyStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */; }; @@ -2371,7 +2368,6 @@ AAA0CC3C25337FAB0079BC96 /* BackForwardListItemViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */; }; AAA0CC472533833C0079BC96 /* MoreOptionsMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC462533833C0079BC96 /* MoreOptionsMenu.swift */; }; AAA0CC572539EBC90079BC96 /* FaviconUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */; }; - AAA0CC6A253CC43C0079BC96 /* WKUserContentControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */; }; AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA892E9250A4CEF005B37B2 /* WindowControllersManager.swift */; }; AAAB9114288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */; }; AAAB9116288EB46B00A057A9 /* VisitMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */; }; @@ -3664,7 +3660,10 @@ 3712092B2C23383C003ADF3D /* RemoteMessagingStoreErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingStoreErrorHandling.swift; sourceTree = ""; }; 3714B1E628EDB7FA0056C57A /* DuckPlayerPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerPreferencesTests.swift; sourceTree = ""; }; 3714B1E828EDBAAB0056C57A /* DuckPlayerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerTests.swift; sourceTree = ""; }; + 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageActionsManagerExtension.swift; sourceTree = ""; }; + 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageWebViewModel.swift; sourceTree = ""; }; 371C0A2827E33EDC0070591F /* FeedbackPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackPresenter.swift; sourceTree = ""; }; + 3720B7F82D00DA4500D20F23 /* WebKitExtensions */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = WebKitExtensions; sourceTree = ""; }; 37219B332CBFBBDB00C9D7A8 /* NewTabPageSearchBoxExperiment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperiment.swift; sourceTree = ""; }; 37219B362CBFBC8200C9D7A8 /* NewTabSearchBoxExperimentPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabSearchBoxExperimentPixel.swift; sourceTree = ""; }; 37219B392CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NewTabPageSearchBoxExperiment+Logger.swift"; sourceTree = ""; }; @@ -3674,7 +3673,6 @@ 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsClientTests.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 = ""; }; @@ -3687,12 +3685,12 @@ 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapter.swift; sourceTree = ""; }; 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModelTests+WithoutPinnedTabsManager.swift"; sourceTree = ""; }; 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyClosedCoordinatorTests.swift; sourceTree = ""; }; + 374F18B32D006F940032EA4E /* NewTabPage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewTabPage; sourceTree = ""; }; 37534C9D28104D9B002621E7 /* TabLazyLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLazyLoaderTests.swift; sourceTree = ""; }; 37534C9F28113101002621E7 /* LazyLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyLoadable.swift; sourceTree = ""; }; 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 /* 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 = ""; }; @@ -3816,10 +3814,7 @@ 37F1E32A2CEF2DA200130142 /* NewTabPageFavoritesActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesActionsHandler.swift; sourceTree = ""; }; 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesClientTests.swift; sourceTree = ""; }; 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverridesMenu.swift; sourceTree = ""; }; - 37F8E2322CEE363D002F0141 /* NewTabPageContextMenuPresenting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageContextMenuPresenting.swift; sourceTree = ""; }; 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesModel.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 = ""; }; 37FC2A0D2CF8DFA00048E226 /* NewTabPagePrivacyStatsModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPagePrivacyStatsModelTests.swift; sourceTree = ""; }; 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyStats.swift; sourceTree = ""; }; 37FC2A1A2CF903A70048E226 /* NewTabPagePrivacyStatsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPagePrivacyStatsClientTests.swift; sourceTree = ""; }; @@ -4514,7 +4509,6 @@ AAA0CC3B25337FAB0079BC96 /* BackForwardListItemViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackForwardListItemViewModel.swift; sourceTree = ""; }; AAA0CC462533833C0079BC96 /* MoreOptionsMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreOptionsMenu.swift; sourceTree = ""; }; AAA0CC562539EBC90079BC96 /* FaviconUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconUserScript.swift; sourceTree = ""; }; - AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKUserContentControllerExtension.swift; sourceTree = ""; }; AAA892E9250A4CEF005B37B2 /* WindowControllersManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowControllersManager.swift; sourceTree = ""; }; AAAB9113288EB1D600A057A9 /* CleanThisHistoryMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanThisHistoryMenuItem.swift; sourceTree = ""; }; AAAB9115288EB46B00A057A9 /* VisitMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitMenuItem.swift; sourceTree = ""; }; @@ -5063,6 +5057,7 @@ 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */, 37269EFD2B332FAC005E8E46 /* Common in Frameworks */, F198C7142BD18A30000BF24D /* PixelKit in Frameworks */, + 37EE81432D00DBCC0068034A /* WebKitExtensions in Frameworks */, F1D43AF52B98E48900BAB743 /* BareBonesBrowserKit in Frameworks */, 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */, 3706FCA7293F65D500E42796 /* BrowserServicesKit in Frameworks */, @@ -5088,6 +5083,7 @@ D6BC8AC82C5A95B10025375B /* DuckPlayer in Frameworks */, 37DF000729F9C061002B7D3E /* SyncDataProviders in Frameworks */, 9D9DE5752C63AA0C00D20B15 /* AppKitExtensions in Frameworks */, + 371BBC592D00C897008FA0C7 /* NewTabPage in Frameworks */, 37BA812F29B3CD6E0053F1A3 /* SyncUI in Frameworks */, 3706FCAF293F65D500E42796 /* PrivacyDashboard in Frameworks */, CD34F0C42C8869FF006826BE /* MaliciousSiteProtection in Frameworks */, @@ -5336,6 +5332,7 @@ 371209232C232E66003ADF3D /* RemoteMessaging in Frameworks */, 37269EFB2B332F9E005E8E46 /* Common in Frameworks */, AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */, + 37EE81412D00DBC40068034A /* WebKitExtensions in Frameworks */, F198C71E2BD18D88000BF24D /* SwiftLintTool in Frameworks */, 1EA7B8D52B7E078C000330A4 /* Subscription in Frameworks */, B6B77BE8297973D4001E68A1 /* Navigation in Frameworks */, @@ -5356,6 +5353,7 @@ 7BA076BB2B65D61400D7FB72 /* NetworkProtectionProxy in Frameworks */, 4B4D60B12A0C83B900BCD287 /* NetworkProtectionUI in Frameworks */, CBECDB8C2CDBD61C005B8B87 /* BrokenSitePrompt in Frameworks */, + 371BBC572D00C891008FA0C7 /* NewTabPage in Frameworks */, 98A50964294B691800D10880 /* Persistence in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5862,16 +5860,13 @@ 376788092CECCAD900F59D83 /* NewTabPage */ = { isa = PBXGroup; children = ( - 37F8E2322CEE363D002F0141 /* NewTabPageContextMenuPresenting.swift */, + 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */, + 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */, 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */, 37CB36892CFA5ADA00E5E5FA /* NextStepsCards */, 37F1E32D2CEF2DDD00130142 /* Favorites */, 37DF370B2CF3CEF5005ED34B /* PrivacyStats */, 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */, - 37FB43102CDB883700479A1E /* NewTabPageActionsManager.swift */, - 372ED7C12CDD4815002287EC /* NewTabPageUserContentController.swift */, - 37FB430D2CDB84A200479A1E /* NewTabPageUserScript.swift */, - 3758CBAA2CE63D510089FC2D /* NewTabPageWebViewModel.swift */, ); path = NewTabPage; sourceTree = ""; @@ -6009,11 +6004,13 @@ 9DF2DB592C73B52F0025F43C /* Freemium */, 9DB6E7222AA0DA7A00A17F3C /* LoginItems */, 7B25FE322AD12C990012AFAB /* NetworkProtectionMac */, + 374F18B32D006F940032EA4E /* NewTabPage */, 378F44E229B4B7B600899924 /* SwiftUIExtensions */, 37BA812B29B3CB8A0053F1A3 /* SyncUI */, 1E862A882A9FC01200F84D4B /* SubscriptionUI */, 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */, 7B8594172B5B25FB0007EB3E /* UDSHelper */, + 3720B7F82D00DA4500D20F23 /* WebKitExtensions */, 7B76E6852AD5D77600186A84 /* XPCHelper */, ); path = LocalPackages; @@ -8941,7 +8938,6 @@ B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, - AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */, 84DDB9092C92B667008C997B /* WKVisitedLinkStoreWrapper.swift */, 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */, B63D466725BEB6C200874977 /* WKWebView+Private.h */, @@ -10031,6 +10027,8 @@ CBECDB8D2CDBD62C005B8B87 /* PageRefreshMonitor */, CBECDB8F2CDBD631005B8B87 /* BrokenSitePrompt */, 37DF37062CF38B9F005ED34B /* PrivacyStats */, + 371BBC582D00C897008FA0C7 /* NewTabPage */, + 37EE81422D00DBCC0068034A /* WebKitExtensions */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -10511,6 +10509,8 @@ CBECDB892CDBD616005B8B87 /* PageRefreshMonitor */, CBECDB8B2CDBD61C005B8B87 /* BrokenSitePrompt */, 37DF37042CF38B96005ED34B /* PrivacyStats */, + 371BBC562D00C891008FA0C7 /* NewTabPage */, + 37EE81402D00DBC40068034A /* WebKitExtensions */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -11434,7 +11434,6 @@ 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 */, @@ -11504,7 +11503,6 @@ 3706FB00293F65D500E42796 /* PasswordManagementCreditCardModel.swift in Sources */, 3706FB01293F65D500E42796 /* NSEventExtension.swift in Sources */, 3706FB02293F65D500E42796 /* Onboarding.swift in Sources */, - 37F8E2342CEE3646002F0141 /* NewTabPageContextMenuPresenting.swift in Sources */, 3706FEB8293F6EFB00E42796 /* ConnectBitwardenView.swift in Sources */, 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */, B60C6F8E29B200AB007BFAA8 /* SavePanelAccessoryView.swift in Sources */, @@ -11539,7 +11537,6 @@ B6E3E55C2BC0041A00A41922 /* DownloadListStoreMock.swift in Sources */, F1D0429A2BFBABA100A31506 /* SubscriptionManager+StandardConfiguration.swift in Sources */, 4B4D60D42A0C84F700BCD287 /* UserText+NetworkProtection.swift in Sources */, - 3707C71F294B5D2900682A9F /* WKUserContentControllerExtension.swift in Sources */, 3706FB1A293F65D500E42796 /* OutlineSeparatorViewCell.swift in Sources */, 3706FB1B293F65D500E42796 /* SafariDataImporter.swift in Sources */, 3199AF702C80734A003AEBDC /* DuckPlayerOnboardingDecider.swift in Sources */, @@ -11591,11 +11588,11 @@ 3706FB36293F65D500E42796 /* PinnedTabView.swift in Sources */, 3706FB37293F65D500E42796 /* DataEncryption.swift in Sources */, 56BA1E762BAAF70F001CF69F /* SpecialErrorPageTabExtension.swift in Sources */, + 371BBC5C2D00C919008FA0C7 /* NewTabPageActionsManagerExtension.swift in Sources */, BD7090D32C52ECFE009EED82 /* UnifiedMetadataCollector.swift in Sources */, 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 */, @@ -11692,6 +11689,7 @@ 3706FB72293F65D500E42796 /* RecentlyClosedCoordinator.swift in Sources */, 3706FB74293F65D500E42796 /* FaviconHostReference.swift in Sources */, B69A14F32B4D6FE800B9417D /* AddBookmarkFolderPopoverViewModel.swift in Sources */, + 371BBC5F2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift in Sources */, 4B67854B2AA8DE76008A5004 /* VPNFeatureGatekeeper.swift in Sources */, C168B9AD2B31DC7F001AFAD9 /* AutofillNeverPromptWebsitesManager.swift in Sources */, 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, @@ -11866,7 +11864,6 @@ 3706FBCD293F65D500E42796 /* LocaleExtension.swift in Sources */, 37D0469E2C7D0EDD00AEAA50 /* CustomBackground.swift in Sources */, 3706FBCE293F65D500E42796 /* SavePaymentMethodViewController.swift in Sources */, - 3758CBAB2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */, 9FA5A0A62BC8F34900153786 /* UserDefaultsBookmarkFoldersStore.swift in Sources */, 1DDC85002B835BC000670238 /* SearchPreferences.swift in Sources */, 7B22D8712CCFD7B7006A76E1 /* TipKitController.swift in Sources */, @@ -12011,7 +12008,6 @@ 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 */, @@ -13261,6 +13257,7 @@ 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */, B6B5F5892B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, B69B503A2726A12500758A2B /* StatisticsLoader.swift in Sources */, + 371BBC5B2D00C919008FA0C7 /* NewTabPageActionsManagerExtension.swift in Sources */, 37CD54C927F2FDD100F1F7B9 /* DataClearingPreferences.swift in Sources */, B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */, 1E25A4FE2CC937120080EFD4 /* SubscriptionCookieManageEventPixelMapping.swift in Sources */, @@ -13280,7 +13277,6 @@ 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 */, @@ -13323,7 +13319,6 @@ AA68C3D32490ED62001B8783 /* NavigationBarViewController.swift in Sources */, AA585DAF2490E6E600E9A3E2 /* MainViewController.swift in Sources */, F1D43AEE2B98D8DF00BAB743 /* MainMenuActions+VanillaBrowser.swift in Sources */, - 3758CBAC2CE63D540089FC2D /* NewTabPageWebViewModel.swift in Sources */, 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */, AA5FA69A275F91C700DCE9C9 /* Favicon.swift in Sources */, AABEE69A24A902A90043105B /* SuggestionContainerViewModel.swift in Sources */, @@ -13453,7 +13448,6 @@ 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 */, @@ -13605,7 +13599,6 @@ 377D8D642C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */, 31CF74572CDC177D004ACCE5 /* AIChatUserScript.swift in Sources */, 1DB9618329F67F6200CF5568 /* FaviconNullStore.swift in Sources */, - 37F8E2332CEE3646002F0141 /* NewTabPageContextMenuPresenting.swift in Sources */, 8400DC4E2C6E2770006509D2 /* SteppedScrollView.swift in Sources */, BB5789722B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, B693954F26F04BEB0015B914 /* PaddedImageButton.swift in Sources */, @@ -13618,6 +13611,7 @@ EA0BA3A9272217E6002A0B6C /* ClickToLoadUserScript.swift in Sources */, AAA892EA250A4CEF005B37B2 /* WindowControllersManager.swift in Sources */, 85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */, + 371BBC5E2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift in Sources */, B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, 379B5AEF2CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */, AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */, @@ -13628,7 +13622,6 @@ AABEE6A924AB4B910043105B /* SuggestionTableCellView.swift in Sources */, AA6820F125503DA9005ED0D5 /* FireViewModel.swift in Sources */, 372BC2A12A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift in Sources */, - AAA0CC6A253CC43C0079BC96 /* WKUserContentControllerExtension.swift in Sources */, 4BE65479271FCD41008D1D63 /* EditableTextView.swift in Sources */, AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */, 1DA84D322C119AE70011C80F /* UpdateMenuItemFactory.swift in Sources */, @@ -13695,7 +13688,6 @@ CB63DECB2CDC0BBE0097986A /* PageRefreshMonitor.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 */, @@ -15619,6 +15611,14 @@ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = RemoteMessaging; }; + 371BBC562D00C891008FA0C7 /* NewTabPage */ = { + isa = XCSwiftPackageProductDependency; + productName = NewTabPage; + }; + 371BBC582D00C897008FA0C7 /* NewTabPage */ = { + isa = XCSwiftPackageProductDependency; + productName = NewTabPage; + }; 371D00E029D8509400EC8598 /* OpenSSL */ = { isa = XCSwiftPackageProductDependency; package = 371D00DF29D8509400EC8598 /* XCRemoteSwiftPackageReference "OpenSSL-XCFramework" */; @@ -15717,6 +15717,14 @@ isa = XCSwiftPackageProductDependency; productName = PrivacyStats; }; + 37EE81402D00DBC40068034A /* WebKitExtensions */ = { + isa = XCSwiftPackageProductDependency; + productName = WebKitExtensions; + }; + 37EE81422D00DBCC0068034A /* WebKitExtensions */ = { + isa = XCSwiftPackageProductDependency; + productName = WebKitExtensions; + }; 37F44A5E298C17830025E7FE /* Navigation */ = { isa = XCSwiftPackageProductDependency; package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index 9316721cce..a028e5de96 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -29,6 +29,7 @@ import FeatureFlags import History import MetricKit import Networking +import NewTabPage import Persistence import PixelKit import ServiceManagement diff --git a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift b/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift index de3366d536..6a73fb3e93 100644 --- a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift +++ b/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift @@ -17,6 +17,7 @@ // import Foundation +import NewTabPage protocol FavoritesActionsHandling { @MainActor func open(_ url: URL, target: NewTabPageFavoritesModel.OpenTarget) diff --git a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift b/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift index 4e53d65c7f..1d2ba28728 100644 --- a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift +++ b/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift @@ -19,6 +19,7 @@ import Bookmarks import Common import Combine +import NewTabPage import UserScript final class NewTabPageFavoritesClient: NewTabPageScriptClient { diff --git a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift b/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift index bd50137f5b..226594c22b 100644 --- a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift +++ b/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift @@ -18,6 +18,7 @@ import Combine import Foundation +import NewTabPage import Persistence protocol NewTabPageFavoritesSettingsPersistor: AnyObject { diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift new file mode 100644 index 0000000000..55154a85e4 --- /dev/null +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -0,0 +1,51 @@ +// +// NewTabPageActionsManagerExtension.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 +import NewTabPage +import PrivacyStats + +extension NewTabPageActionsManager { + + convenience init( + appearancePreferences: AppearancePreferences, + activeRemoteMessageModel: ActiveRemoteMessageModel, + privacyStats: PrivacyStatsCollecting, + openURLHandler: @escaping (URL) -> Void + ) { + let privacyStatsModel = NewTabPagePrivacyStatsModel( + privacyStats: privacyStats, + trackerDataProvider: PrivacyStatsTrackerDataProvider(contentBlocking: ContentBlocking.shared) + ) + + self.init(scriptClients: [ + NewTabPageConfigurationClient(appearancePreferences: appearancePreferences), + NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel, openURLHandler: openURLHandler), + NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), + NewTabPageFavoritesClient(favoritesModel: NewTabPageFavoritesModel()), + NewTabPagePrivacyStatsClient(model: privacyStatsModel) + ]) + } +} + +struct NewTabPageTabOpener: ContinueSetUpModelTabOpening { + @MainActor + func openTab(_ tab: Tab) { + WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.insertOrAppend(tab: tab, selected: true) + } +} diff --git a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift b/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift index 41f88e5f84..d863ee0408 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift @@ -20,6 +20,7 @@ import AppKit import Combine import Common import os.log +import NewTabPage import UserScript final class NewTabPageConfigurationClient: NewTabPageScriptClient { diff --git a/DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift b/DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift index b68373dca3..38315b5b02 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift @@ -18,6 +18,7 @@ import Combine import Common +import NewTabPage import RemoteMessaging import UserScript diff --git a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift index 0d3fcb3294..e8eb6fdef0 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift @@ -18,6 +18,7 @@ import BrowserServicesKit import Combine +import NewTabPage import WebKit /** @@ -70,3 +71,18 @@ extension NewTabPageWebViewModel: WKNavigationDelegate { extension Notification.Name { static var newTabPageWebViewDidAppear = Notification.Name("newTabPageWebViewDidAppear") } + +extension WKWebViewConfiguration { + + @MainActor + func applyNewTabPageWebViewConfiguration(with featureFlagger: FeatureFlagger, newTabPageUserScript: NewTabPageUserScript) { + if urlSchemeHandler(forURLScheme: URL.NavigationalScheme.duck.rawValue) == nil { + setURLSchemeHandler( + DuckURLSchemeHandler(featureFlagger: featureFlagger, isNTPSpecialPageSupported: true), + forURLScheme: URL.NavigationalScheme.duck.rawValue + ) + } + preferences[.developerExtrasEnabled] = true + self.userContentController = NewTabPageUserContentController(newTabPageUserScript: newTabPageUserScript) + } +} diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index f377d1dbde..0cbec22d3f 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -18,6 +18,7 @@ import Common import Combine +import NewTabPage import UserScript final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift index d508f01d2c..7ceac496f1 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -18,8 +18,9 @@ import Common import Combine -import UserScript +import NewTabPage import PixelKit +import UserScript protocol NewTabPageNextStepsCardsProviding: AnyObject { var isViewExpanded: Bool { get set } diff --git a/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift b/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift index 74ccca3476..edd372cb77 100644 --- a/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift +++ b/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift @@ -19,6 +19,7 @@ import Combine import Common import os.log +import NewTabPage import UserScript final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { diff --git a/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift b/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift index 39771e5164..6af77ff349 100644 --- a/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift +++ b/DuckDuckGo/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift @@ -18,6 +18,7 @@ import Combine import os.log +import NewTabPage import Persistence import PrivacyStats diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 4fbb30cf21..88d3928293 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -21,14 +21,15 @@ import Cocoa import Combine import Common import FeatureFlags -import SwiftUI -import WebKit -import Subscription -import PixelKit -import os.log -import Onboarding import Freemium +import NewTabPage +import Onboarding +import os.log +import PixelKit +import Subscription +import SwiftUI import UserScript +import WebKit protocol BrowserTabViewControllerDelegate: AnyObject { func highlightFireButton() diff --git a/LocalPackages/NewTabPage/.gitignore b/LocalPackages/NewTabPage/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/LocalPackages/NewTabPage/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift new file mode 100644 index 0000000000..25708dfd11 --- /dev/null +++ b/LocalPackages/NewTabPage/Package.swift @@ -0,0 +1,52 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Package.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 PackageDescription + +let package = Package( + name: "NewTabPage", + platforms: [ + .macOS("11.4") + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "NewTabPage", + targets: ["NewTabPage"]), + ], + dependencies: [ + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.0"), + .package(path: "../WebKitExtensions"), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "NewTabPage", + dependencies: [ + .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), + .product(name: "WebKitExtensions", package: "WebKitExtensions"), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)) + ] + ), + ] +) diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift similarity index 66% rename from DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift index 474eda2e8b..6903b36648 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManager.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift @@ -18,9 +18,6 @@ import Foundation import Combine -import PixelKit -import PrivacyStats -import RemoteMessaging import Common import os.log @@ -34,7 +31,7 @@ import os.log * * Objects implementing this protocol are added to `NewTabPageActionsManager`. */ -protocol NewTabPageScriptClient: AnyObject { +public protocol NewTabPageScriptClient: AnyObject { /** * Handle to the object that returns the list of all living `NewTabPageUserScript` instances. */ @@ -46,7 +43,7 @@ protocol NewTabPageScriptClient: AnyObject { func registerMessageHandlers(for userScript: SubfeatureWithExternalMessageHandling) } -extension NewTabPageScriptClient { +public extension NewTabPageScriptClient { /** * Convenience method to push a message with specific parameters to all user scripts * currently registered with `userScriptsSource`. @@ -66,14 +63,14 @@ extension NewTabPageScriptClient { * * It's conformed to by `NewTabPageActionsManager` (via `NewTabPageActionsManaging`). */ -protocol NewTabPageUserScriptsSource: AnyObject { +public protocol NewTabPageUserScriptsSource: AnyObject { var userScripts: [NewTabPageUserScript] { get } } /** * This protocol describes the API of `NewTabPageActionsManager`. */ -protocol NewTabPageActionsManaging: AnyObject, NewTabPageUserScriptsSource { +public protocol NewTabPageActionsManaging: AnyObject, NewTabPageUserScriptsSource { func registerUserScript(_ userScript: NewTabPageUserScript) } @@ -85,7 +82,7 @@ protocol NewTabPageActionsManaging: AnyObject, NewTabPageUserScriptsSource { * of NTP data sources, this class keeps track of all living NTP user scripts and makes sure * script clients' message handlers are registered with all user scripts. */ -final class NewTabPageActionsManager: NewTabPageActionsManaging, NewTabPageUserScriptsSource { +public final class NewTabPageActionsManager: NewTabPageActionsManaging, NewTabPageUserScriptsSource { private let newTabPageScriptClients: [NewTabPageScriptClient] @@ -95,7 +92,7 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging, NewTabPageUserS */ private let userScriptsHandles = NSHashTable.weakObjects() - var userScripts: [NewTabPageUserScript] { + public var userScripts: [NewTabPageUserScript] { userScriptsHandles.allObjects } @@ -103,43 +100,13 @@ final class NewTabPageActionsManager: NewTabPageActionsManaging, NewTabPageUserS * Records user script reference internally and register all clients' message handlers * with the user script. */ - func registerUserScript(_ userScript: NewTabPageUserScript) { + public func registerUserScript(_ userScript: NewTabPageUserScript) { userScriptsHandles.add(userScript) newTabPageScriptClients.forEach { $0.registerMessageHandlers(for: userScript) } } - init(scriptClients: [NewTabPageScriptClient]) { + public init(scriptClients: [NewTabPageScriptClient]) { newTabPageScriptClients = scriptClients newTabPageScriptClients.forEach { $0.userScriptsSource = self } } } - -extension NewTabPageActionsManager { - - convenience init( - appearancePreferences: AppearancePreferences, - activeRemoteMessageModel: ActiveRemoteMessageModel, - privacyStats: PrivacyStatsCollecting, - openURLHandler: @escaping (URL) -> Void - ) { - let privacyStatsModel = NewTabPagePrivacyStatsModel( - privacyStats: privacyStats, - trackerDataProvider: PrivacyStatsTrackerDataProvider(contentBlocking: ContentBlocking.shared) - ) - - self.init(scriptClients: [ - NewTabPageConfigurationClient(appearancePreferences: appearancePreferences), - NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel, openURLHandler: openURLHandler), - NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), - NewTabPageFavoritesClient(favoritesModel: NewTabPageFavoritesModel()), - NewTabPagePrivacyStatsClient(model: privacyStatsModel) - ]) - } -} - -struct NewTabPageTabOpener: ContinueSetUpModelTabOpening { - @MainActor - func openTab(_ tab: Tab) { - WindowControllersManager.shared.lastKeyMainWindowController?.mainViewController.tabCollectionViewModel.insertOrAppend(tab: tab, selected: true) - } -} diff --git a/DuckDuckGo/NewTabPage/NewTabPageContextMenuPresenting.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageContextMenuPresenting.swift similarity index 80% rename from DuckDuckGo/NewTabPage/NewTabPageContextMenuPresenting.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageContextMenuPresenting.swift index 612039af99..88c1335689 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageContextMenuPresenting.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageContextMenuPresenting.swift @@ -18,14 +18,16 @@ import AppKit -protocol NewTabPageContextMenuPresenting { +public protocol NewTabPageContextMenuPresenting { func showContextMenu(_ menu: NSMenu) } -struct DefaultNewTabPageContextMenuPresenter: NewTabPageContextMenuPresenting { - func showContextMenu(_ menu: NSMenu) { +public struct DefaultNewTabPageContextMenuPresenter: NewTabPageContextMenuPresenting { + public func showContextMenu(_ menu: NSMenu) { if !menu.items.isEmpty { menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil) } } + + public init() {} } diff --git a/DuckDuckGo/NewTabPage/NewTabPageUserContentController.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift similarity index 64% rename from DuckDuckGo/NewTabPage/NewTabPageUserContentController.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift index de555cb150..4d8f732d73 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageUserContentController.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift @@ -20,13 +20,14 @@ import Foundation import WebKit import BrowserServicesKit import UserScript +import WebKitExtensions -final class NewTabPageUserContentController: WKUserContentController { +public final class NewTabPageUserContentController: WKUserContentController { - let newTabPageUserScriptProvider: NewTabPageUserScriptProvider + public let newTabPageUserScriptProvider: NewTabPageUserScriptProvider @MainActor - init(newTabPageUserScript: NewTabPageUserScript) { + public init(newTabPageUserScript: NewTabPageUserScript) { newTabPageUserScriptProvider = NewTabPageUserScriptProvider(newTabPageUserScript: newTabPageUserScript) super.init() @@ -49,18 +50,18 @@ final class NewTabPageUserContentController: WKUserContentController { } @MainActor -final class NewTabPageUserScriptProvider: UserScriptsProvider { - lazy var userScripts: [UserScript] = [specialPagesUserScript] +public final class NewTabPageUserScriptProvider: UserScriptsProvider { + public lazy var userScripts: [UserScript] = [specialPagesUserScript] - let specialPagesUserScript: SpecialPagesUserScript + public let specialPagesUserScript: SpecialPagesUserScript - init(newTabPageUserScript: NewTabPageUserScript) { + public init(newTabPageUserScript: NewTabPageUserScript) { specialPagesUserScript = SpecialPagesUserScript() specialPagesUserScript.registerSubfeature(delegate: newTabPageUserScript) } @MainActor - func loadWKUserScripts() async -> [WKUserScript] { + public func loadWKUserScripts() async -> [WKUserScript] { return await withTaskGroup(of: WKUserScriptBox.self) { @MainActor group in var wkUserScripts = [WKUserScript]() userScripts.forEach { userScript in @@ -76,18 +77,3 @@ final class NewTabPageUserScriptProvider: UserScriptsProvider { } } } - -extension WKWebViewConfiguration { - - @MainActor - func applyNewTabPageWebViewConfiguration(with featureFlagger: FeatureFlagger, newTabPageUserScript: NewTabPageUserScript) { - if urlSchemeHandler(forURLScheme: URL.NavigationalScheme.duck.rawValue) == nil { - setURLSchemeHandler( - DuckURLSchemeHandler(featureFlagger: featureFlagger, isNTPSpecialPageSupported: true), - forURLScheme: URL.NavigationalScheme.duck.rawValue - ) - } - preferences[.developerExtrasEnabled] = true - self.userContentController = NewTabPageUserContentController(newTabPageUserScript: newTabPageUserScript) - } -} diff --git a/DuckDuckGo/NewTabPage/NewTabPageUserScript.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift similarity index 56% rename from DuckDuckGo/NewTabPage/NewTabPageUserScript.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift index 365fc10cb7..1663e99604 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageUserScript.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift @@ -30,16 +30,16 @@ import WebKit * registered as handlers to handle feature-specific messages, e.g. a separate object * responsible for RMF, favorites, privacy stats, etc. */ -protocol SubfeatureWithExternalMessageHandling: AnyObject, Subfeature { +public protocol SubfeatureWithExternalMessageHandling: AnyObject, Subfeature { var webView: WKWebView? { get } func registerMessageHandlers(_ handlers: [String: Subfeature.Handler]) } -final class NewTabPageUserScript: NSObject, SubfeatureWithExternalMessageHandling { +public final class NewTabPageUserScript: NSObject, SubfeatureWithExternalMessageHandling { - var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "newtab")]) - let featureName: String = "newTabPage" - weak var broker: UserScriptMessageBroker? + public var messageOriginPolicy: MessageOriginPolicy = .only(rules: [.exact(hostname: "newtab")]) + public let featureName: String = "newTabPage" + public weak var broker: UserScriptMessageBroker? public func with(broker: UserScriptMessageBroker) { self.broker = broker @@ -47,20 +47,20 @@ final class NewTabPageUserScript: NSObject, SubfeatureWithExternalMessageHandlin // MARK: - Message Handling - typealias MessageName = String + public typealias MessageName = String - weak var webView: WKWebView? + public weak var webView: WKWebView? private var messageHandlers: [MessageName: Handler] = [:] - func registerMessageHandlers(_ handlers: [MessageName: Subfeature.Handler]) { + public func registerMessageHandlers(_ handlers: [MessageName: Subfeature.Handler]) { messageHandlers.merge(handlers, uniquingKeysWith: { $1 }) } - func handler(forMethodNamed methodName: MessageName) -> Handler? { + public func handler(forMethodNamed methodName: MessageName) -> Handler? { messageHandlers[methodName] } - func pushMessage(named method: String, params: Encodable?, using script: NewTabPageUserScript) { + public func pushMessage(named method: String, params: Encodable?, using script: NewTabPageUserScript) { guard let webView = script.webView else { return } @@ -68,24 +68,33 @@ final class NewTabPageUserScript: NSObject, SubfeatureWithExternalMessageHandlin } } -extension NewTabPageUserScript { +public extension NewTabPageUserScript { - struct WidgetConfig: Codable { - let animation: Animation? - let expansion: Expansion + public struct WidgetConfig: Codable { + public let animation: Animation? + public let expansion: Expansion - enum Expansion: String, Codable { + public init(animation: Animation?, expansion: Expansion) { + self.animation = animation + self.expansion = expansion + } + + public enum Expansion: String, Codable { case collapsed, expanded } - struct Animation: Codable, Equatable { - let kind: AnimationKind + public struct Animation: Codable, Equatable { + public let kind: AnimationKind + + public init(kind: AnimationKind) { + self.kind = kind + } - static let none = Animation(kind: .none) - static let viewTransitions = Animation(kind: .viewTransitions) - static let auto = Animation(kind: .auto) + public static let none = Animation(kind: .none) + public static let viewTransitions = Animation(kind: .viewTransitions) + public static let auto = Animation(kind: .auto) - enum AnimationKind: String, Codable { + public enum AnimationKind: String, Codable { case none case viewTransitions = "view-transitions" case auto = "auto-animate" diff --git a/LocalPackages/WebKitExtensions/.gitignore b/LocalPackages/WebKitExtensions/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/LocalPackages/WebKitExtensions/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift new file mode 100644 index 0000000000..14cf5e4ece --- /dev/null +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -0,0 +1,48 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// +// Package.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 PackageDescription + +let package = Package( + name: "WebKitExtensions", + platforms: [ + .macOS("11.4") + ], + products: [ + .library( + name: "WebKitExtensions", + targets: ["WebKitExtensions"] + ), + ], + dependencies: [ + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.0"), + ], + targets: [ + .target( + name: "WebKitExtensions", + dependencies: [ + .product(name: "UserScript", package: "BrowserServicesKit"), + ], + swiftSettings: [ + .define("DEBUG", .when(configuration: .debug)), + ] + ) + ] +) diff --git a/DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift b/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/WKUserContentControllerExtension.swift similarity index 97% rename from DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift rename to LocalPackages/WebKitExtensions/Sources/WebKitExtensions/WKUserContentControllerExtension.swift index 0dea968eba..0d821fc7fe 100644 --- a/DuckDuckGo/Common/Extensions/WKUserContentControllerExtension.swift +++ b/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/WKUserContentControllerExtension.swift @@ -21,7 +21,7 @@ import WebKit import UserScript @MainActor -extension WKUserContentController { +public extension WKUserContentController { func addHandler(_ userScript: UserScript) { for messageName in userScript.messageNames { diff --git a/LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift b/LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift new file mode 100644 index 0000000000..dd641d88f8 --- /dev/null +++ b/LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift @@ -0,0 +1,6 @@ +import Testing +@testable import WebKitExtensions + +@Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. +} From 23cb5b4e1190cc56d89f48fd8ef6e72ea0bcfc00 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 4 Dec 2024 21:30:56 +0100 Subject: [PATCH 13/62] Move Next Steps cards --- DuckDuckGo.xcodeproj/project.pbxproj | 26 +++--------- ...ft => ContinueSetUpModel+NewTabPage.swift} | 19 +-------- .../NewTabPageNextStepsCardsClient.swift | 40 +++++++++++-------- .../NewTabPageNextStepsCardsProviding.swift | 38 ++++++++++++++++++ 4 files changed, 70 insertions(+), 53 deletions(-) rename DuckDuckGo/NewTabPage/{NextStepsCards/NewTabPageNextStepsCardsProviding.swift => ContinueSetUpModel+NewTabPage.swift} (83%) rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift (83%) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 42dfac7c0b..caa901a992 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1112,8 +1112,6 @@ 372217822B33380700B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 372217812B33380700B8E9C2 /* TestUtils */; }; 3723A94F2CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; 3723A9502CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; - 3724ED8B2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */; }; - 3724ED8C2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */; }; 3724ED8E2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */; }; 3724ED8F2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */; }; 37269EFB2B332F9E005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFA2B332F9E005E8E46 /* Common */; }; @@ -1125,6 +1123,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 */; }; + 372D15E92D00F19500A11576 /* ContinueSetUpModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */; }; + 372D15EA2D00F19500A11576 /* ContinueSetUpModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.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 */; }; @@ -1253,8 +1253,6 @@ 37BF3F22286F0A7A00BD9014 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; 37C9F78C2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; 37C9F78D2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; - 37CB368B2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */; }; - 37CB368C2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */; }; 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; 37CC53EC27E8A4D10028713D /* PreferencesDataClearingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */; }; @@ -3669,10 +3667,10 @@ 37219B392CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NewTabPageSearchBoxExperiment+Logger.swift"; sourceTree = ""; }; 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperimentTests.swift; sourceTree = ""; }; 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClient.swift; sourceTree = ""; }; - 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsProviding.swift; sourceTree = ""; }; 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsClientTests.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 = ""; }; + 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContinueSetUpModel+NewTabPage.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 = ""; }; @@ -3766,7 +3764,6 @@ 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsTabExtension.swift; sourceTree = ""; }; - 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsClient.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; 37CC53F327E8D4620028713D /* NSPathControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSPathControlView.swift; sourceTree = ""; }; @@ -5860,10 +5857,10 @@ 376788092CECCAD900F59D83 /* NewTabPage */ = { isa = PBXGroup; children = ( + 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */, 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */, 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */, 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */, - 37CB36892CFA5ADA00E5E5FA /* NextStepsCards */, 37F1E32D2CEF2DDD00130142 /* Favorites */, 37DF370B2CF3CEF5005ED34B /* PrivacyStats */, 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */, @@ -6045,15 +6042,6 @@ path = PrivacyStats; sourceTree = ""; }; - 37CB36892CFA5ADA00E5E5FA /* NextStepsCards */ = { - isa = PBXGroup; - children = ( - 3724ED8A2CFE6A120043626A /* NewTabPageNextStepsCardsProviding.swift */, - 37CB368A2CFA5AEC00E5E5FA /* NewTabPageNextStepsCardsClient.swift */, - ); - path = NextStepsCards; - sourceTree = ""; - }; 37CD54C027F2FDD100F1F7B9 /* Model */ = { isa = PBXGroup; children = ( @@ -11716,7 +11704,6 @@ 3706FB80293F65D500E42796 /* NSAlert+ActiveDownloadsTermination.swift in Sources */, B677FC552B064A9C0099EB04 /* DataImportViewModel.swift in Sources */, D64A5FF92AEA5C2B00B6D6E7 /* HomeButtonMenuFactory.swift in Sources */, - 3724ED8B2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */, 4BE3A6C22C16BEB1003FC378 /* VPNRedditSessionWorkaround.swift in Sources */, 3707C717294B5D0F00682A9F /* FindInPageTabExtension.swift in Sources */, 3706FB81293F65D500E42796 /* IndexPathExtension.swift in Sources */, @@ -12159,6 +12146,7 @@ B6C0BB6829AEFF8100AE8E3C /* BookmarkExtension.swift in Sources */, B69A14FB2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */, 3706FC6C293F65D500E42796 /* BookmarkViewModel.swift in Sources */, + 372D15EA2D00F19500A11576 /* ContinueSetUpModel+NewTabPage.swift in Sources */, 3706FC6D293F65D500E42796 /* DaxSpeech.swift in Sources */, CB63DECC2CDC0BBE0097986A /* PageRefreshMonitor.swift in Sources */, 3706FC6E293F65D500E42796 /* DuckURLSchemeHandler.swift in Sources */, @@ -12175,7 +12163,6 @@ 3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */, 4BF0E5132AD25A2600FFEC9E /* DuckDuckGoUserAgent.swift in Sources */, 3706FC78293F65D500E42796 /* SecureVaultReporter.swift in Sources */, - 37CB368C2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */, 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */, 3706FEBD293F6EFF00E42796 /* BWCommand.swift in Sources */, C172E7302C9329D300521D9A /* FlippedView.swift in Sources */, @@ -13091,7 +13078,6 @@ B62B483E2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, 4B92929B26670D2A00AD2C21 /* BookmarkOutlineViewDataSource.swift in Sources */, 56D145EB29E6C99B00E3488A /* DataImportStatusProviding.swift in Sources */, - 37CB368B2CFA5AF100E5E5FA /* NewTabPageNextStepsCardsClient.swift in Sources */, 843965122C6F2FFE004C8899 /* NSDragOperationExtension.swift in Sources */, 31D5375C291D944100407A95 /* PasswordManagementBitwardenItemView.swift in Sources */, 1D9297BF2C1B062900A38521 /* ApplicationUpdateDetector.swift in Sources */, @@ -13109,7 +13095,6 @@ B6E3E5542BBFCEE300A41922 /* NoDownloadsCellView.swift in Sources */, 3768D8382C24BFF5004120AE /* RemoteMessageView.swift in Sources */, 4BBDEE9428FC14760092FAA6 /* ConnectBitwardenViewController.swift in Sources */, - 3724ED8C2CFE6A290043626A /* NewTabPageNextStepsCardsProviding.swift in Sources */, 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */, 9833912F27AAA3CE00DAF119 /* AppTrackerDataSetProvider.swift in Sources */, B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */, @@ -13406,6 +13391,7 @@ 3199AF792C80734A003AEBDC /* DuckPlayerOnboardingViewModel.swift in Sources */, B6685E4229A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, 4B8AC93B26B48ADF00879451 /* ASN1Parser.swift in Sources */, + 372D15E92D00F19500A11576 /* ContinueSetUpModel+NewTabPage.swift in Sources */, 856C98DF257014BD00A22F1F /* FileDownloadManager.swift in Sources */, 4BB99CFF26FE191E001E4761 /* BookmarkImport.swift in Sources */, B68503A7279141CD00893A05 /* KeySetDictionary.swift in Sources */, diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift similarity index 83% rename from DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift rename to DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift index 7ceac496f1..2b1487d626 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift @@ -1,5 +1,5 @@ // -// NewTabPageNextStepsCardsProviding.swift +// ContinueSetUpModel+NewTabPage.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -22,22 +22,6 @@ import NewTabPage import PixelKit import UserScript -protocol NewTabPageNextStepsCardsProviding: AnyObject { - var isViewExpanded: Bool { get set } - var isViewExpandedPublisher: AnyPublisher { get } - - var cards: [NewTabPageNextStepsCardsClient.CardID] { get } - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } - - @MainActor - func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) - - @MainActor - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) - - func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) -} - extension HomePage.Models.ContinueSetUpModel: NewTabPageNextStepsCardsProviding { var isViewExpanded: Bool { get { @@ -117,3 +101,4 @@ extension NewTabPageNextStepsCardsClient.CardID { } } } + diff --git a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift similarity index 83% rename from DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index 0cbec22d3f..cbd469ac95 100644 --- a/DuckDuckGo/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -18,14 +18,14 @@ import Common import Combine -import NewTabPage import UserScript +import WebKit -final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { +public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { - let model: NewTabPageNextStepsCardsProviding - let willDisplayCardsPublisher: AnyPublisher<[CardID], Never> - weak var userScriptsSource: NewTabPageUserScriptsSource? + public let model: NewTabPageNextStepsCardsProviding + public let willDisplayCardsPublisher: AnyPublisher<[CardID], Never> + public weak var userScriptsSource: NewTabPageUserScriptsSource? private let willDisplayCardsSubject = PassthroughSubject<[CardID], Never>() private let getDataSubject = PassthroughSubject<[CardID], Never>() @@ -34,7 +34,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { private let notifyConfigUpdatedSubject = PassthroughSubject() private var cancellables: Set = [] - init(model: NewTabPageNextStepsCardsProviding) { + public init(model: NewTabPageNextStepsCardsProviding) { self.model = model willDisplayCardsPublisher = willDisplayCardsSubject.eraseToAnyPublisher() connectWillDisplayCardsPublisher() @@ -96,7 +96,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { .store(in: &cancellables) } - enum MessageName: String, CaseIterable { + public enum MessageName: String, CaseIterable { case action = "nextSteps_action" case dismiss = "nextSteps_dismiss" case getConfig = "nextSteps_getConfig" @@ -106,7 +106,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { case setConfig = "nextSteps_setConfig" } - func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + public func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { userScript.registerMessageHandlers([ MessageName.action.rawValue: { [weak self] in try await self?.action(params: $0, original: $1) }, MessageName.dismiss.rawValue: { [weak self] in try await self?.dismiss(params: $0, original: $1) }, @@ -117,7 +117,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let card: NewTabPageNextStepsCardsClient.Card = DecodableHelper.decode(from: params) else { return nil } @@ -126,7 +126,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let card: Card = DecodableHelper.decode(from: params) else { return nil } @@ -134,7 +134,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { return nil } - func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = model.isViewExpanded ? .expanded : .collapsed getConfigSubject.send(model.isViewExpanded) @@ -142,7 +142,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { return nil } @@ -151,7 +151,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let cardIDs = model.cards let cards = cardIDs.map(Card.init(id:)) @@ -178,7 +178,7 @@ final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } } -extension NewTabPageNextStepsCardsClient { +public extension NewTabPageNextStepsCardsClient { enum CardID: String, Codable { case bringStuff @@ -189,10 +189,18 @@ extension NewTabPageNextStepsCardsClient { } struct Card: Codable, Equatable { - let id: CardID + public let id: CardID + + public init(id: CardID) { + self.id = id + } } struct NextStepsData: Codable, Equatable { - let content: [Card]? + public let content: [Card]? + + public init(content: [Card]?) { + self.content = content + } } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift new file mode 100644 index 0000000000..48322a7640 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -0,0 +1,38 @@ +// +// NewTabPageNextStepsCardsProviding.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 Common +import Combine +import PixelKit +import UserScript + +public protocol NewTabPageNextStepsCardsProviding: AnyObject { + var isViewExpanded: Bool { get set } + var isViewExpandedPublisher: AnyPublisher { get } + + var cards: [NewTabPageNextStepsCardsClient.CardID] { get } + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } + + @MainActor + func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) + + @MainActor + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) + + func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) +} From 1b21b41c3069f90acb2167fef0651479736df32b Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 4 Dec 2024 21:42:53 +0100 Subject: [PATCH 14/62] Move some tests --- DuckDuckGo.xcodeproj/project.pbxproj | 18 ---------- .../DuckDuckGo Privacy Browser.xcscheme | 10 ++++++ LocalPackages/NewTabPage/Package.swift | 6 ++++ .../Helpers/NewTabPageTestsHelper.swift | 35 +++++++++++++++++++ .../NewTabPageActionsManagerTests.swift | 2 +- .../NewTabPageNextStepsCardsClientTests.swift | 2 +- .../NewTabPageUserScriptTests.swift | 2 +- .../WebKitExtensionsTests.swift | 6 ---- .../NewTabPageConfigurationClientTests.swift | 1 + .../NewTabPageFavoritesClientTests.swift | 1 + .../NewTabPagePrivacyStatsClientTests.swift | 1 + .../NewTabPage/NewTabPageRMFClientTests.swift | 1 + 12 files changed, 58 insertions(+), 27 deletions(-) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/NewTabPageTestsHelper.swift rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageActionsManagerTests.swift (98%) rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageNextStepsCardsClientTests.swift (99%) rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageUserScriptTests.swift (98%) delete mode 100644 LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index caa901a992..b6e66205cb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1112,8 +1112,6 @@ 372217822B33380700B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 372217812B33380700B8E9C2 /* TestUtils */; }; 3723A94F2CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; 3723A9502CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; - 3724ED8E2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */; }; - 3724ED8F2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */; }; 37269EFB2B332F9E005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFA2B332F9E005E8E46 /* Common */; }; 37269EFD2B332FAC005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFC2B332FAC005E8E46 /* Common */; }; 37269EFF2B332FBB005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFE2B332FBB005E8E46 /* Common */; }; @@ -1168,10 +1166,6 @@ 3767319F2C7F416200EB097B /* CustomBackgroundTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3767319B2C7F415200EB097B /* CustomBackgroundTests.swift */; }; 376731A12C7F50D200EB097B /* Logger+HomePageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */; }; 376731A22C7F50D200EB097B /* Logger+HomePageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */; }; - 3767880C2CECCB7200F59D83 /* NewTabPageActionsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3767880B2CECCB6C00F59D83 /* NewTabPageActionsManagerTests.swift */; }; - 3767880D2CECCB7200F59D83 /* NewTabPageActionsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3767880B2CECCB6C00F59D83 /* NewTabPageActionsManagerTests.swift */; }; - 3767880F2CECD5A800F59D83 /* NewTabPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3767880E2CECD5A200F59D83 /* NewTabPageUserScriptTests.swift */; }; - 376788102CECD5A800F59D83 /* NewTabPageUserScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3767880E2CECD5A200F59D83 /* NewTabPageUserScriptTests.swift */; }; 376788122CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */; }; 376788132CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */; }; 376788152CED308200F59D83 /* NewTabPageConfigurationClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788142CED308000F59D83 /* NewTabPageConfigurationClientTests.swift */; }; @@ -3667,7 +3661,6 @@ 37219B392CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NewTabPageSearchBoxExperiment+Logger.swift"; sourceTree = ""; }; 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperimentTests.swift; sourceTree = ""; }; 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClient.swift; sourceTree = ""; }; - 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageNextStepsCardsClientTests.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 = ""; }; 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContinueSetUpModel+NewTabPage.swift"; sourceTree = ""; }; @@ -3700,8 +3693,6 @@ 376731962C7F36AA00EB097B /* UserBackgroundImageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBackgroundImageTests.swift; sourceTree = ""; }; 3767319B2C7F415200EB097B /* CustomBackgroundTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBackgroundTests.swift; sourceTree = ""; }; 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+HomePageSettings.swift"; sourceTree = ""; }; - 3767880B2CECCB6C00F59D83 /* NewTabPageActionsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageActionsManagerTests.swift; sourceTree = ""; }; - 3767880E2CECD5A200F59D83 /* NewTabPageUserScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageUserScriptTests.swift; sourceTree = ""; }; 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClientTests.swift; sourceTree = ""; }; 376788142CED308000F59D83 /* NewTabPageConfigurationClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageConfigurationClientTests.swift; sourceTree = ""; }; 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageTestsHelper.swift; sourceTree = ""; }; @@ -5872,10 +5863,7 @@ isa = PBXGroup; children = ( 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */, - 3767880E2CECD5A200F59D83 /* NewTabPageUserScriptTests.swift */, - 3767880B2CECCB6C00F59D83 /* NewTabPageActionsManagerTests.swift */, 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */, - 3724ED8D2CFE6B330043626A /* NewTabPageNextStepsCardsClientTests.swift */, 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */, 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */, 37FC2A1A2CF903A70048E226 /* NewTabPagePrivacyStatsClientTests.swift */, @@ -12408,7 +12396,6 @@ 567A23E22C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, 378D62572CEF80200056BBD8 /* NewTabPageFavoritesModelTests.swift in Sources */, - 3767880F2CECD5A800F59D83 /* NewTabPageUserScriptTests.swift in Sources */, 56A0540E2C1C375E007D8FAB /* MockWindow.swift in Sources */, 3706FE2A293F661700E42796 /* SafariVersionReaderTests.swift in Sources */, 31031EB82CC94C6E00684340 /* AIChatRemoteSettingsTests.swift in Sources */, @@ -12487,7 +12474,6 @@ 3706FE4E293F661700E42796 /* BookmarkListTests.swift in Sources */, 3706FE4F293F661700E42796 /* BookmarksExporterTests.swift in Sources */, 566B196629CDB829007E38F4 /* CapturingOptionsButtonMenuDelegate.swift in Sources */, - 3767880C2CECCB7200F59D83 /* NewTabPageActionsManagerTests.swift in Sources */, 3706FE50293F661700E42796 /* WindowManagerStateRestorationTests.swift in Sources */, 3706FE51293F661700E42796 /* SafariBookmarksReaderTests.swift in Sources */, 3706FE52293F661700E42796 /* FileSystemDSLTests.swift in Sources */, @@ -12590,7 +12576,6 @@ CD33012A2C887B1C009AA127 /* URLTokenValidatorTests.swift in Sources */, 9F0660742BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, 9FBD847B2BB3EC3300220859 /* MockAttributionOriginProvider.swift in Sources */, - 3724ED8F2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */, 56A214B02CB583BF00E5BC0E /* TrackerMessageProviderTests.swift in Sources */, 3706FE82293F661700E42796 /* MockStatisticsStore.swift in Sources */, 9FBD84712BB3DD8400220859 /* MockAttributionsPixelHandler.swift in Sources */, @@ -14097,7 +14082,6 @@ 843965152C737022004C8899 /* NSPasteboardExtension.swift in Sources */, 4B9292C22667103100AD2C21 /* BookmarkTests.swift in Sources */, 5601FECD29B7973D00068905 /* TabBarViewItemTests.swift in Sources */, - 3767880D2CECCB7200F59D83 /* NewTabPageActionsManagerTests.swift in Sources */, 1D8C2FE52B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift in Sources */, 142879DC24CE1185005419BB /* SuggestionContainerViewModelTests.swift in Sources */, 566B195D29CDB692007E38F4 /* MoreOptionsMenuTests.swift in Sources */, @@ -14108,7 +14092,6 @@ B6106BAF26A7C6180013B453 /* PermissionStoreMock.swift in Sources */, 4B98D27A28D95F1A003C2B6F /* ChromiumFaviconsReaderTests.swift in Sources */, 9F0660732BECC71200B8EEF1 /* SubscriptionAttributionPixelHandlerTests.swift in Sources */, - 3724ED8E2CFE6B3F0043626A /* NewTabPageNextStepsCardsClientTests.swift in Sources */, BDCB66D82C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */, 567A23E12C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 986189E62A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, @@ -14128,7 +14111,6 @@ 9FBD84772BB3E54200220859 /* InstallationAttributionPixelHandlerTests.swift in Sources */, AA9C363025518CA9004B1BA3 /* FireTests.swift in Sources */, B6106BB126A7D8720013B453 /* PermissionStoreTests.swift in Sources */, - 376788102CECD5A800F59D83 /* NewTabPageUserScriptTests.swift in Sources */, 4BF4951826C08395000547B8 /* ThirdPartyBrowserTests.swift in Sources */, 4B98D27C28D960DD003C2B6F /* FirefoxFaviconsReaderTests.swift in Sources */, 9F0FFFBE2BCCAF1F007C87DD /* BookmarkAllTabsDialogViewModelMock.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme index b272e5bb34..409a1bb0bf 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme @@ -271,6 +271,16 @@ ReferencedContainer = "container:LocalPackages/AppKitExtensions"> + + + + Any { + if JSONSerialization.isValidJSONObject(value) { + return value + } + if let encodableValue = value as? Encodable { + let jsonData = try JSONEncoder().encode(encodableValue) + return try JSONSerialization.jsonObject(with: jsonData) + } + XCTFail("invalid JSON value", file: file, line: line) + return [] + } +} diff --git a/UnitTests/NewTabPage/NewTabPageActionsManagerTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift similarity index 98% rename from UnitTests/NewTabPage/NewTabPageActionsManagerTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift index fd57f2d104..dddc979744 100644 --- a/UnitTests/NewTabPage/NewTabPageActionsManagerTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift @@ -17,7 +17,7 @@ // import XCTest -@testable import DuckDuckGo_Privacy_Browser +@testable import NewTabPage final class MockNewTabPageScriptClient: NewTabPageScriptClient { var userScriptsSource: NewTabPageUserScriptsSource? diff --git a/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift similarity index 99% rename from UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift index a8ee7acec4..e23edb530c 100644 --- a/UnitTests/NewTabPage/NewTabPageNextStepsCardsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift @@ -19,7 +19,7 @@ import Combine import TestUtils import XCTest -@testable import DuckDuckGo_Privacy_Browser +@testable import NewTabPage final class CapturingNewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding { diff --git a/UnitTests/NewTabPage/NewTabPageUserScriptTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageUserScriptTests.swift similarity index 98% rename from UnitTests/NewTabPage/NewTabPageUserScriptTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageUserScriptTests.swift index 4d7c57e250..fe25438aa6 100644 --- a/UnitTests/NewTabPage/NewTabPageUserScriptTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageUserScriptTests.swift @@ -18,7 +18,7 @@ import WebKit import XCTest -@testable import DuckDuckGo_Privacy_Browser +@testable import NewTabPage final class NewTabPageUserScriptTests: XCTestCase { diff --git a/LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift b/LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift deleted file mode 100644 index dd641d88f8..0000000000 --- a/LocalPackages/WebKitExtensions/Tests/WebKitExtensionsTests/WebKitExtensionsTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing -@testable import WebKitExtensions - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} diff --git a/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift b/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift index 3659c80511..f27667d336 100644 --- a/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift @@ -18,6 +18,7 @@ import AppKit import Combine +import NewTabPage import XCTest @testable import DuckDuckGo_Privacy_Browser diff --git a/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift b/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift index 446b7cc0bf..6798faeb8f 100644 --- a/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift @@ -17,6 +17,7 @@ // import Combine +import NewTabPage import RemoteMessaging import TestUtils import XCTest diff --git a/UnitTests/NewTabPage/NewTabPagePrivacyStatsClientTests.swift b/UnitTests/NewTabPage/NewTabPagePrivacyStatsClientTests.swift index a61c877a6c..bff90e8a0f 100644 --- a/UnitTests/NewTabPage/NewTabPagePrivacyStatsClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPagePrivacyStatsClientTests.swift @@ -17,6 +17,7 @@ // import Combine +import NewTabPage import PrivacyStats import TestUtils import TrackerRadarKit diff --git a/UnitTests/NewTabPage/NewTabPageRMFClientTests.swift b/UnitTests/NewTabPage/NewTabPageRMFClientTests.swift index 8f0563f434..11d1cd8489 100644 --- a/UnitTests/NewTabPage/NewTabPageRMFClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPageRMFClientTests.swift @@ -17,6 +17,7 @@ // import Combine +import NewTabPage import RemoteMessaging import XCTest @testable import DuckDuckGo_Privacy_Browser From 28747cd3ee4b63f5d0b42d21b40f99eec98c3768 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 4 Dec 2024 23:40:06 +0100 Subject: [PATCH 15/62] Move some tests to the package --- DuckDuckGo.xcodeproj/project.pbxproj | 18 +-- .../AppearancePreferences+NewTabPage.swift | 48 +++++++ .../NewTabPageActionsManagerExtension.swift | 2 +- .../NewTabPageConfigurationClient.swift | 120 ++++++++++-------- ...turingNewTabPageContextMenuPresenter.swift | 28 ++++ ...ewTabPageSectionsVisibilityProviding.swift | 32 +++++ .../NewTabPageConfigurationClientTests.swift | 40 ++---- .../NewTabPageFavoritesClientTests.swift | 8 ++ 8 files changed, 202 insertions(+), 94 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/AppearancePreferences+NewTabPage.swift rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/NewTabPageConfigurationClient.swift (61%) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageContextMenuPresenter.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageSectionsVisibilityProviding.swift rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageConfigurationClientTests.swift (77%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b6e66205cb..8f1f9fb6f4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1123,6 +1123,8 @@ 372BC2A22A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */; }; 372D15E92D00F19500A11576 /* ContinueSetUpModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */; }; 372D15EA2D00F19500A11576 /* ContinueSetUpModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */; }; + 372D15EC2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */; }; + 372D15ED2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.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 */; }; @@ -1168,8 +1170,6 @@ 376731A22C7F50D200EB097B /* Logger+HomePageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */; }; 376788122CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */; }; 376788132CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */; }; - 376788152CED308200F59D83 /* NewTabPageConfigurationClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788142CED308000F59D83 /* NewTabPageConfigurationClientTests.swift */; }; - 376788162CED308200F59D83 /* NewTabPageConfigurationClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788142CED308000F59D83 /* NewTabPageConfigurationClientTests.swift */; }; 376788182CED4C4100F59D83 /* NewTabPageTestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */; }; 376788192CED4C4100F59D83 /* NewTabPageTestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */; }; 3768D8382C24BFF5004120AE /* RemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3768D8372C24BFF5004120AE /* RemoteMessageView.swift */; }; @@ -1214,8 +1214,6 @@ 3797C7A02C61806500DA77FB /* HomePageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3797C79F2C61806500DA77FB /* HomePageSettingsView.swift */; }; 3797C7A32C62C38800DA77FB /* HomePageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3797C7A22C62C38800DA77FB /* HomePageSettingsModel.swift */; }; 3797C7A42C62C38800DA77FB /* HomePageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3797C7A22C62C38800DA77FB /* HomePageSettingsModel.swift */; }; - 379B5AEC2CEA26DA00B9F5D7 /* NewTabPageConfigurationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */; }; - 379B5AED2CEA26DA00B9F5D7 /* NewTabPageConfigurationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */; }; 379B5AEF2CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */; }; 379B5AF02CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */; }; 379B5AF22CEA32FF00B9F5D7 /* NewTabPagePrivacyStatsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AF12CEA32FF00B9F5D7 /* NewTabPagePrivacyStatsClient.swift */; }; @@ -3664,6 +3662,7 @@ 372A0FEB2B2379310033BF7F /* SyncMetricsEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMetricsEventsHandler.swift; sourceTree = ""; }; 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCredentialsAdapter.swift; sourceTree = ""; }; 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContinueSetUpModel+NewTabPage.swift"; sourceTree = ""; }; + 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppearancePreferences+NewTabPage.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 = ""; }; @@ -3694,7 +3693,6 @@ 3767319B2C7F415200EB097B /* CustomBackgroundTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBackgroundTests.swift; sourceTree = ""; }; 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+HomePageSettings.swift"; sourceTree = ""; }; 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClientTests.swift; sourceTree = ""; }; - 376788142CED308000F59D83 /* NewTabPageConfigurationClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageConfigurationClientTests.swift; sourceTree = ""; }; 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageTestsHelper.swift; sourceTree = ""; }; 3768D8372C24BFF5004120AE /* RemoteMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessageView.swift; sourceTree = ""; }; 3768D83A2C24C0A8004120AE /* RemoteMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessageViewModel.swift; sourceTree = ""; }; @@ -3733,7 +3731,6 @@ 379026D02C650CCD00A089E8 /* UserBackgroundImagesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBackgroundImagesManager.swift; sourceTree = ""; }; 3797C79F2C61806500DA77FB /* HomePageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsView.swift; sourceTree = ""; }; 3797C7A22C62C38800DA77FB /* HomePageSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsModel.swift; sourceTree = ""; }; - 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageConfigurationClient.swift; sourceTree = ""; }; 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesClient.swift; sourceTree = ""; }; 379B5AF12CEA32FF00B9F5D7 /* NewTabPagePrivacyStatsClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPagePrivacyStatsClient.swift; sourceTree = ""; }; 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesAutofillView.swift; sourceTree = ""; }; @@ -5848,13 +5845,13 @@ 376788092CECCAD900F59D83 /* NewTabPage */ = { isa = PBXGroup; children = ( + 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */, 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */, 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */, 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */, 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */, 37F1E32D2CEF2DDD00130142 /* Favorites */, 37DF370B2CF3CEF5005ED34B /* PrivacyStats */, - 379B5AEB2CEA26C600B9F5D7 /* NewTabPageConfigurationClient.swift */, ); path = NewTabPage; sourceTree = ""; @@ -5868,7 +5865,6 @@ 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */, 37FC2A1A2CF903A70048E226 /* NewTabPagePrivacyStatsClientTests.swift */, 37FC2A0D2CF8DFA00048E226 /* NewTabPagePrivacyStatsModelTests.swift */, - 376788142CED308000F59D83 /* NewTabPageConfigurationClientTests.swift */, ); path = NewTabPage; sourceTree = ""; @@ -11482,6 +11478,7 @@ 3706FEB8293F6EFB00E42796 /* ConnectBitwardenView.swift in Sources */, 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */, B60C6F8E29B200AB007BFAA8 /* SavePanelAccessoryView.swift in Sources */, + 372D15ED2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */, 4B9579222AC687170062CA31 /* HardwareModel.swift in Sources */, 3706FB03293F65D500E42796 /* PopUpWindow.swift in Sources */, CB24F70D29A3D9CB006DCC58 /* AppConfigurationURLProvider.swift in Sources */, @@ -12205,7 +12202,6 @@ 3706FC9A293F65D500E42796 /* AutoconsentManagement.swift in Sources */, 3706FC9C293F65D500E42796 /* BookmarkStore.swift in Sources */, 3706FC9D293F65D500E42796 /* PrivacyDashboardViewController.swift in Sources */, - 379B5AEC2CEA26DA00B9F5D7 /* NewTabPageConfigurationClient.swift in Sources */, 841BE93C2C6F1CB200E9C2B5 /* MenuItemNode.swift in Sources */, B6A22B632B1E29D000ECD2BA /* DataImportSummaryViewModel.swift in Sources */, 56DB9FED2CD2515F001BEC23 /* OnboardingPixelReporter.swift in Sources */, @@ -12405,7 +12401,6 @@ 3706FE2D293F661700E42796 /* ChromiumFaviconsReaderTests.swift in Sources */, 3706FE2E293F661700E42796 /* LocalBookmarkManagerTests.swift in Sources */, 9F180D132B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */, - 376788162CED308200F59D83 /* NewTabPageConfigurationClientTests.swift in Sources */, 3706FE2F293F661700E42796 /* WebViewMock.swift in Sources */, 3706FE30293F661700E42796 /* CollectionExtension.swift in Sources */, B630E80129C887ED00363609 /* NSErrorAdditionalInfo.swift in Sources */, @@ -13538,7 +13533,6 @@ 4BE4005527CF3F19007D3161 /* SavePaymentMethodViewController.swift in Sources */, 1D2DC009290167A0008083A1 /* BWStatus.swift in Sources */, AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */, - 379B5AED2CEA26DA00B9F5D7 /* NewTabPageConfigurationClient.swift in Sources */, 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, B63D467A25BFC3E100874977 /* NSCoderExtensions.swift in Sources */, 1D2DC00B290167EC008083A1 /* RunningApplicationCheck.swift in Sources */, @@ -13783,6 +13777,7 @@ AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, CD2AB5C12C8222F40019EB49 /* MaliciousSiteProtectionPreferences.swift in Sources */, + 372D15EC2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */, C1DAF3B52B9A44860059244F /* AutofillPopoverPresenter.swift in Sources */, B687B7CA2947A029001DEA6F /* ContentBlockingTabExtension.swift in Sources */, 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */, @@ -14150,7 +14145,6 @@ 562532A02BC069180034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 4B9292C02667103100AD2C21 /* BookmarkManagedObjectTests.swift in Sources */, 373A1AB228451ED400586521 /* BookmarksHTMLImporterTests.swift in Sources */, - 376788152CED308200F59D83 /* NewTabPageConfigurationClientTests.swift in Sources */, 4B723E0626B0003E00E14D75 /* CSVParserTests.swift in Sources */, 561D29C62BDA74ED007B91D0 /* MockDDGSyncing.swift in Sources */, B60C6F8429B1BAD3007BFAA8 /* FileManagerTempDirReplacement.swift in Sources */, diff --git a/DuckDuckGo/NewTabPage/AppearancePreferences+NewTabPage.swift b/DuckDuckGo/NewTabPage/AppearancePreferences+NewTabPage.swift new file mode 100644 index 0000000000..132787500f --- /dev/null +++ b/DuckDuckGo/NewTabPage/AppearancePreferences+NewTabPage.swift @@ -0,0 +1,48 @@ +// +// AppearancePreferences+NewTabPage.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 NewTabPage + +extension AppearancePreferences: NewTabPageSectionsVisibilityProviding { + var isFavoritesVisible: Bool { + get { + isFavoriteVisible + } + set { + isFavoriteVisible = newValue + } + } + + var isPrivacyStatsVisible: Bool { + get { + isRecentActivityVisible + } + set { + isRecentActivityVisible = newValue + } + } + + var isFavoritesVisiblePublisher: AnyPublisher { + $isFavoriteVisible.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + var isPrivacyStatsVisiblePublisher: AnyPublisher { + $isRecentActivityVisible.dropFirst().removeDuplicates().eraseToAnyPublisher() + } +} diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 55154a85e4..1ff65de79f 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -34,7 +34,7 @@ extension NewTabPageActionsManager { ) self.init(scriptClients: [ - NewTabPageConfigurationClient(appearancePreferences: appearancePreferences), + NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel, openURLHandler: openURLHandler), NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), NewTabPageFavoritesClient(favoritesModel: NewTabPageFavoritesModel()), diff --git a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift similarity index 61% rename from DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index d863ee0408..e104da11a8 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -20,40 +20,41 @@ import AppKit import Combine import Common import os.log -import NewTabPage import UserScript +import WebKit -final class NewTabPageConfigurationClient: NewTabPageScriptClient { +public protocol NewTabPageSectionsVisibilityProviding: AnyObject { + var isFavoritesVisible: Bool { get set } + var isPrivacyStatsVisible: Bool { get set } - weak var userScriptsSource: NewTabPageUserScriptsSource? + var isFavoritesVisiblePublisher: AnyPublisher { get } + var isPrivacyStatsVisiblePublisher: AnyPublisher { get } +} + +public final class NewTabPageConfigurationClient: NewTabPageScriptClient { + + public weak var userScriptsSource: NewTabPageUserScriptsSource? private var cancellables = Set() - private let appearancePreferences: AppearancePreferences + private let sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding private let contextMenuPresenter: NewTabPageContextMenuPresenting - init( - appearancePreferences: AppearancePreferences, + public init( + sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding, contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter() ) { - self.appearancePreferences = appearancePreferences + self.sectionsVisibilityProvider = sectionsVisibilityProvider self.contextMenuPresenter = contextMenuPresenter - appearancePreferences.$isFavoriteVisible.dropFirst().removeDuplicates().asVoid() + Publishers.Merge(sectionsVisibilityProvider.isFavoritesVisiblePublisher, sectionsVisibilityProvider.isPrivacyStatsVisiblePublisher) .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 + .sink { [weak self] _ in self?.notifyWidgetConfigsDidChange() } .store(in: &cancellables) } - enum MessageName: String, CaseIterable { + public enum MessageName: String, CaseIterable { case contextMenu case initialSetup case reportInitException @@ -62,7 +63,7 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { case widgetsOnConfigUpdated = "widgets_onConfigUpdated" } - func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + public func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { userScript.registerMessageHandlers([ MessageName.contextMenu.rawValue: { [weak self] in try await self?.showContextMenu(params: $0, original: $1) }, MessageName.initialSetup.rawValue: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, @@ -74,8 +75,8 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { private func notifyWidgetConfigsDidChange() { let widgetConfigs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ - .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), - .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) + .init(id: .favorites, isVisible: sectionsVisibilityProvider.isFavoritesVisible), + .init(id: .privacyStats, isVisible: sectionsVisibilityProvider.isPrivacyStatsVisible) ] pushMessage(named: MessageName.widgetsOnConfigUpdated.rawValue, params: widgetConfigs) @@ -90,14 +91,16 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { for menuItem in params.visibilityMenuItems { switch menuItem.id { case .favorites: - let item = NSMenuItem(title: menuItem.title, action: #selector(toggleVisibility(_:)), representedObject: menuItem.id) - .targetting(self) - item.state = appearancePreferences.isFavoriteVisible ? .on : .off + let item = NSMenuItem(title: menuItem.title, action: #selector(self.toggleVisibility(_:)), keyEquivalent: "") + item.target = self + item.representedObject = menuItem.id + item.state = sectionsVisibilityProvider.isFavoritesVisible ? .on : .off menu.addItem(item) case .privacyStats: - let item = NSMenuItem(title: menuItem.title, action: #selector(toggleVisibility(_:)), representedObject: menuItem.id) - .targetting(self) - item.state = appearancePreferences.isRecentActivityVisible ? .on : .off + let item = NSMenuItem(title: menuItem.title, action: #selector(self.toggleVisibility(_:)), keyEquivalent: "") + item.target = self + item.representedObject = menuItem.id + item.state = sectionsVisibilityProvider.isPrivacyStatsVisible ? .on : .off menu.addItem(item) default: break @@ -114,9 +117,9 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { @objc private func toggleVisibility(_ sender: NSMenuItem) { switch sender.representedObject as? NewTabPageUserScript.WidgetId { case .favorites: - appearancePreferences.isFavoriteVisible.toggle() + sectionsVisibilityProvider.isFavoritesVisible.toggle() case .privacyStats: - appearancePreferences.isRecentActivityVisible.toggle() + sectionsVisibilityProvider.isPrivacyStatsVisible.toggle() default: break } @@ -137,8 +140,8 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { .init(id: .privacyStats) ], widgetConfigs: [ - .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), - .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) + .init(id: .favorites, isVisible: sectionsVisibilityProvider.isFavoritesVisible), + .init(id: .privacyStats, isVisible: sectionsVisibilityProvider.isPrivacyStatsVisible) ], env: env, locale: Bundle.main.preferredLocalizations.first ?? "en", @@ -154,9 +157,9 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { for widgetConfig in widgetConfigs { switch widgetConfig.id { case .favorites: - appearancePreferences.isFavoriteVisible = widgetConfig.visibility.isVisible + sectionsVisibilityProvider.isFavoritesVisible = widgetConfig.visibility.isVisible case .privacyStats: - appearancePreferences.isRecentActivityVisible = widgetConfig.visibility.isVisible + sectionsVisibilityProvider.isPrivacyStatsVisible = widgetConfig.visibility.isVisible default: break } @@ -164,7 +167,7 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { return nil } - func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [String: String] else { return nil } let message = params["message"] ?? "" let id = params["id"] ?? "" @@ -173,53 +176,62 @@ final class NewTabPageConfigurationClient: NewTabPageScriptClient { } } -extension NewTabPageUserScript { +public extension NewTabPageUserScript { enum WidgetId: String, Codable { case rmf, nextSteps, favorites, privacyStats } struct ContextMenuParams: Codable { - let visibilityMenuItems: [ContextMenuItem] + public let visibilityMenuItems: [ContextMenuItem] + + public init(visibilityMenuItems: [ContextMenuItem]) { + self.visibilityMenuItems = visibilityMenuItems + } - struct ContextMenuItem: Codable { - let id: WidgetId - let title: String + public struct ContextMenuItem: Codable { + public let id: WidgetId + public let title: String + + public init(id: WidgetId, title: String) { + self.id = id + self.title = title + } } } struct NewTabPageConfiguration: Encodable { - var widgets: [Widget] - var widgetConfigs: [WidgetConfig] - var env: String - var locale: String - var platform: Platform - - struct Widget: Encodable, Equatable { - var id: WidgetId + public var widgets: [Widget] + public var widgetConfigs: [WidgetConfig] + public var env: String + public var locale: String + public var platform: Platform + + public struct Widget: Encodable, Equatable { + public var id: WidgetId } - struct WidgetConfig: Codable, Equatable { + public struct WidgetConfig: Codable, Equatable { - enum WidgetVisibility: String, Codable { + public enum WidgetVisibility: String, Codable { case visible, hidden - var isVisible: Bool { + public var isVisible: Bool { self == .visible } } - init(id: WidgetId, isVisible: Bool) { + public init(id: WidgetId, isVisible: Bool) { self.id = id self.visibility = isVisible ? .visible : .hidden } - var id: WidgetId - var visibility: WidgetVisibility + public var id: WidgetId + public var visibility: WidgetVisibility } - struct Platform: Encodable, Equatable { - var name: String + public struct Platform: Encodable, Equatable { + public var name: String } } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageContextMenuPresenter.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageContextMenuPresenter.swift new file mode 100644 index 0000000000..9d31f67647 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageContextMenuPresenter.swift @@ -0,0 +1,28 @@ +// +// CapturingNewTabPageContextMenuPresenter.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 +import NewTabPage + +final class CapturingNewTabPageContextMenuPresenter: NewTabPageContextMenuPresenting { + func showContextMenu(_ menu: NSMenu) { + showContextMenuCalls.append(menu) + } + + var showContextMenuCalls: [NSMenu] = [] +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageSectionsVisibilityProviding.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageSectionsVisibilityProviding.swift new file mode 100644 index 0000000000..50f5408b7f --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageSectionsVisibilityProviding.swift @@ -0,0 +1,32 @@ +// +// MockNewTabPageSectionsVisibilityProviding.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 NewTabPage + +final class MockNewTabPageSectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding { + @Published var isFavoritesVisible: Bool = true + @Published var isPrivacyStatsVisible: Bool = true + + var isFavoritesVisiblePublisher: AnyPublisher { + $isFavoritesVisible.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + var isPrivacyStatsVisiblePublisher: AnyPublisher { + $isPrivacyStatsVisible.dropFirst().removeDuplicates().eraseToAnyPublisher() + } +} diff --git a/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift similarity index 77% rename from UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift index f27667d336..8f5edf319b 100644 --- a/UnitTests/NewTabPage/NewTabPageConfigurationClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift @@ -18,30 +18,21 @@ import AppKit import Combine -import NewTabPage import XCTest -@testable import DuckDuckGo_Privacy_Browser - -final class CapturingNewTabPageContextMenuPresenter: NewTabPageContextMenuPresenting { - func showContextMenu(_ menu: NSMenu) { - showContextMenuCalls.append(menu) - } - - var showContextMenuCalls: [NSMenu] = [] -} +@testable import NewTabPage final class NewTabPageConfigurationClientTests: XCTestCase { var client: NewTabPageConfigurationClient! - var appearancePreferences: AppearancePreferences! + var sectionsVisibilityProvider: MockNewTabPageSectionsVisibilityProvider! var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! var userScript: NewTabPageUserScript! override func setUpWithError() throws { try super.setUpWithError() - appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock()) + sectionsVisibilityProvider = MockNewTabPageSectionsVisibilityProvider() contextMenuPresenter = CapturingNewTabPageContextMenuPresenter() client = NewTabPageConfigurationClient( - appearancePreferences: appearancePreferences, + sectionsVisibilityProvider: sectionsVisibilityProvider, contextMenuPresenter: contextMenuPresenter ) @@ -53,8 +44,8 @@ final class NewTabPageConfigurationClientTests: XCTestCase { @MainActor func testThatContextMenuShowsContextMenu() async throws { - appearancePreferences.isFavoriteVisible = true - appearancePreferences.isRecentActivityVisible = false + sectionsVisibilityProvider.isFavoritesVisible = true + sectionsVisibilityProvider.isPrivacyStatsVisible = false let parameters = NewTabPageUserScript.ContextMenuParams(visibilityMenuItems: [ .init(id: .favorites, title: "Favorites"), @@ -69,11 +60,6 @@ final class NewTabPageConfigurationClientTests: XCTestCase { XCTAssertEqual(menu.items[0].state, .on) XCTAssertEqual(menu.items[1].title, "Privacy Stats") XCTAssertEqual(menu.items[1].state, .off) - - menu.performActionForItem(at: 0) - XCTAssertFalse(appearancePreferences.isFavoriteVisible) - menu.performActionForItem(at: 1) - XCTAssertTrue(appearancePreferences.isRecentActivityVisible) } func testWhenContextMenuParamsIsEmptyThenContextMenuDoesNotShow() async throws { @@ -94,8 +80,8 @@ final class NewTabPageConfigurationClientTests: XCTestCase { .init(id: .privacyStats) ]) XCTAssertEqual(configuration.widgetConfigs, [ - .init(id: .favorites, isVisible: appearancePreferences.isFavoriteVisible), - .init(id: .privacyStats, isVisible: appearancePreferences.isRecentActivityVisible) + .init(id: .favorites, isVisible: sectionsVisibilityProvider.isFavoritesVisible), + .init(id: .privacyStats, isVisible: sectionsVisibilityProvider.isPrivacyStatsVisible) ]) XCTAssertEqual(configuration.platform, .init(name: "macos")) } @@ -108,19 +94,19 @@ final class NewTabPageConfigurationClientTests: XCTestCase { .init(id: .privacyStats, isVisible: true) ] try await sendMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) - XCTAssertEqual(appearancePreferences.isFavoriteVisible, false) - XCTAssertEqual(appearancePreferences.isRecentActivityVisible, true) + XCTAssertEqual(sectionsVisibilityProvider.isFavoritesVisible, false) + XCTAssertEqual(sectionsVisibilityProvider.isPrivacyStatsVisible, true) } func testWhenWidgetsSetConfigIsReceivedWithPartialConfigThenOnlyIncludedWidgetsConfigsAreUpdated() async throws { - let initialIsFavoritesVisible = appearancePreferences.isFavoriteVisible + let initialIsFavoritesVisible = sectionsVisibilityProvider.isFavoritesVisible let configs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ .init(id: .privacyStats, isVisible: false) ] try await sendMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) - XCTAssertEqual(appearancePreferences.isFavoriteVisible, initialIsFavoritesVisible) - XCTAssertEqual(appearancePreferences.isRecentActivityVisible, false) + XCTAssertEqual(sectionsVisibilityProvider.isFavoritesVisible, initialIsFavoritesVisible) + XCTAssertEqual(sectionsVisibilityProvider.isPrivacyStatsVisible, false) } // MARK: - Helper functions diff --git a/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift b/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift index 6798faeb8f..eb20beb26a 100644 --- a/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift +++ b/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift @@ -23,6 +23,14 @@ import TestUtils import XCTest @testable import DuckDuckGo_Privacy_Browser +final class CapturingNewTabPageContextMenuPresenter: NewTabPageContextMenuPresenting { + func showContextMenu(_ menu: NSMenu) { + showContextMenuCalls.append(menu) + } + + var showContextMenuCalls: [NSMenu] = [] +} + final class CapturingNewTabPageFavoritesActionsHandler: FavoritesActionsHandling { struct OpenCall: Equatable { let url: URL From 772d49f60be2f6b4ab9a2b24838102438c6fef54 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 09:32:16 +0100 Subject: [PATCH 16/62] Re-add migrating recently visited view expanded state --- .../NewTabPageActionsManagerExtension.swift | 3 ++- .../NewTabPagePrivacyStatsModel.swift | 20 +++++++++---------- .../NewTabPagePrivacyStatsClientTests.swift | 2 +- .../NewTabPagePrivacyStatsModelTests.swift | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 80d054b66e..d0d0c1a5b7 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -31,7 +31,8 @@ extension NewTabPageActionsManager { let privacyStatsModel = NewTabPagePrivacyStatsModel( privacyStats: privacyStats, trackerDataProvider: PrivacyStatsTrackerDataProvider(contentBlocking: ContentBlocking.shared), - keyValueStore: UserDefaults.standard + keyValueStore: UserDefaults.standard, + getLegacyIsViewExpandedSetting: UserDefaultsWrapper(key: .homePageShowRecentlyVisited, defaultValue: false).wrappedValue ) self.init(scriptClients: [ diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift index b60ec59b4c..99b3d4d5d8 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift @@ -32,9 +32,9 @@ public final class UserDefaultsNewTabPagePrivacyStatsSettingsPersistor: NewTabPa private let keyValueStore: KeyValueStoring - public init(_ keyValueStore: KeyValueStoring) { + public init(_ keyValueStore: KeyValueStoring, getLegacySetting: @autoclosure () -> Bool?) { self.keyValueStore = keyValueStore - migrateFromNativeHomePageSettings() + migrateFromLegacyHomePageSettings(using: getLegacySetting) } public var isViewExpanded: Bool { @@ -42,12 +42,11 @@ public final class UserDefaultsNewTabPagePrivacyStatsSettingsPersistor: NewTabPa set { keyValueStore.set(newValue, forKey: Keys.isViewExpanded) } } - private func migrateFromNativeHomePageSettings() { -// guard keyValueStore.object(forKey: Keys.isViewExpanded) == nil else { -// return -// } -// let legacyKey = UserDefaultsWrapper.Key.homePageShowRecentlyVisited.rawValue -// isViewExpanded = keyValueStore.object(forKey: legacyKey) as? Bool ?? false + private func migrateFromLegacyHomePageSettings(using getLegacySetting: () -> Bool?) { + guard keyValueStore.object(forKey: Keys.isViewExpanded) == nil, let legacySetting = getLegacySetting() else { + return + } + isViewExpanded = legacySetting } } @@ -72,12 +71,13 @@ public final class NewTabPagePrivacyStatsModel { public convenience init( privacyStats: PrivacyStatsCollecting, trackerDataProvider: PrivacyStatsTrackerDataProviding, - keyValueStore: KeyValueStoring + keyValueStore: KeyValueStoring, + getLegacyIsViewExpandedSetting: @autoclosure () -> Bool? ) { self.init( privacyStats: privacyStats, trackerDataProvider: trackerDataProvider, - settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor(keyValueStore) + settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor(keyValueStore, getLegacySetting: getLegacyIsViewExpandedSetting()) ) } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift index 968b6cc440..992a7548ec 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift @@ -38,7 +38,7 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { privacyStats = CapturingPrivacyStats() trackerDataProvider = MockPrivacyStatsTrackerDataProvider() - settingsPersistor = UserDefaultsNewTabPagePrivacyStatsSettingsPersistor(MockKeyValueStore()) + settingsPersistor = UserDefaultsNewTabPagePrivacyStatsSettingsPersistor(MockKeyValueStore(), getLegacySetting: nil) model = NewTabPagePrivacyStatsModel( privacyStats: privacyStats, diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift index 0e3de52f12..3f1b450d2f 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift @@ -77,7 +77,7 @@ final class NewTabPagePrivacyStatsModelTests: XCTestCase { privacyStats = CapturingPrivacyStats() trackerDataProvider = MockPrivacyStatsTrackerDataProvider() - settingsPersistor = UserDefaultsNewTabPagePrivacyStatsSettingsPersistor(MockKeyValueStore()) + settingsPersistor = UserDefaultsNewTabPagePrivacyStatsSettingsPersistor(MockKeyValueStore(), getLegacySetting: nil) model = NewTabPagePrivacyStatsModel( privacyStats: privacyStats, trackerDataProvider: trackerDataProvider, From f6394b243abfe1d79bfde039400bcadef9f04522 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 09:44:27 +0100 Subject: [PATCH 17/62] Move Favorites --- DuckDuckGo.xcodeproj/project.pbxproj | 78 ++++------- .../NSAttributedStringExtension.swift | 1 + .../Extensions/NSMenuItemExtension.swift | 86 ------------ .../Common/Extensions/URLExtension.swift | 5 - .../View/SwiftUI/NSPopUpButtonView.swift | 1 + ...t => DefaultsFavoritesActionHandler.swift} | 35 ++--- .../NewTabPageActionsManagerExtension.swift | 5 +- .../TabExtensions/ContextMenuManager.swift | 2 +- LocalPackages/AppKitExtensions/Package.swift | 2 + .../NSAccessibilityProtocolExtension.swift | 2 +- .../AppKitExtensions}/NSEventExtension.swift | 40 +++--- .../AppKitExtensions}/NSMenuExtension.swift | 7 +- .../NSMenuItemExtension.swift | 108 +++++++++++++++ .../NewTabPageFavoritesActionsHandler.swift | 39 ++++++ .../Favorites/NewTabPageFavoritesClient.swift | 124 ++++++++++++------ .../Favorites/NewTabPageFavoritesModel.swift | 91 ++++++------- .../NewTabPage/internal/UserText.swift | 27 ++++ .../NewTabPageTests/DuckFaviconURLTests.swift | 44 +++++++ LocalPackages/Utilities/.gitignore | 8 ++ LocalPackages/Utilities/Package.swift | 18 +++ .../Sources}/Utilities/ArrayBuilder.swift | 30 ++--- LocalPackages/WebKitExtensions/Package.swift | 2 + .../WKMenuItemIdentifier.swift | 12 +- .../Sources/WebKitExtensions/export.swift | 19 +++ 24 files changed, 482 insertions(+), 304 deletions(-) rename DuckDuckGo/NewTabPage/{Favorites/NewTabPageFavoritesActionsHandler.swift => DefaultsFavoritesActionHandler.swift} (79%) rename {DuckDuckGo/Common/Extensions => LocalPackages/AppKitExtensions/Sources/AppKitExtensions}/NSAccessibilityProtocolExtension.swift (96%) rename {DuckDuckGo/Common/Extensions => LocalPackages/AppKitExtensions/Sources/AppKitExtensions}/NSEventExtension.swift (82%) rename {DuckDuckGo/Common/Extensions => LocalPackages/AppKitExtensions/Sources/AppKitExtensions}/NSMenuExtension.swift (92%) create mode 100644 LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuItemExtension.swift create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/Favorites/NewTabPageFavoritesClient.swift (59%) rename {DuckDuckGo => LocalPackages/NewTabPage/Sources}/NewTabPage/Favorites/NewTabPageFavoritesModel.swift (64%) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/internal/UserText.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift create mode 100644 LocalPackages/Utilities/.gitignore create mode 100644 LocalPackages/Utilities/Package.swift rename {DuckDuckGo/Common => LocalPackages/Utilities/Sources}/Utilities/ArrayBuilder.swift (55%) rename {DuckDuckGo/Common/Extensions => LocalPackages/WebKitExtensions/Sources/WebKitExtensions}/WKMenuItemIdentifier.swift (89%) create mode 100644 LocalPackages/WebKitExtensions/Sources/WebKitExtensions/export.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d374341f7d..672aeb29d3 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -316,7 +316,6 @@ 31A83FB82BE28D8A00F74E67 /* UserText+DBP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */; }; 31AA6B972B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; 31AA6B982B960BA50025014E /* DataBrokerProtectionLoginItemPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */; }; - 31B4AF532901A4F20013585E /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B4AF522901A4F20013585E /* NSEventExtension.swift */; }; 31C26A0A2CBE9D2700FFF462 /* PreferencesAIChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C26A092CBE9D2200FFF462 /* PreferencesAIChat.swift */; }; 31C26A0B2CBE9D2700FFF462 /* PreferencesAIChat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C26A092CBE9D2200FFF462 /* PreferencesAIChat.swift */; }; 31C26A0D2CBE9DFE00FFF462 /* AIChatPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C26A0C2CBE9DFA00FFF462 /* AIChatPreferences.swift */; }; @@ -466,7 +465,6 @@ 3706FAFD293F65D500E42796 /* DownloadsPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6B1E87D26D5DA0E0062C350 /* DownloadsPopover.swift */; }; 3706FAFE293F65D500E42796 /* SpacerNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929626670D2A00AD2C21 /* SpacerNode.swift */; }; 3706FB00293F65D500E42796 /* PasswordManagementCreditCardModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE6547B271FCD4D008D1D63 /* PasswordManagementCreditCardModel.swift */; }; - 3706FB01293F65D500E42796 /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B4AF522901A4F20013585E /* NSEventExtension.swift */; }; 3706FB02293F65D500E42796 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F25276A335700DC0649 /* Onboarding.swift */; }; 3706FB03293F65D500E42796 /* PopUpWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C0274E3EF4002AC6B0 /* PopUpWindow.swift */; }; 3706FB05293F65D500E42796 /* Favicons.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69E275F948900DCE9C9 /* Favicons.xcdatamodeld */; }; @@ -773,7 +771,6 @@ 3706FC79293F65D500E42796 /* NSImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */; }; 3706FC7B293F65D500E42796 /* PasswordManagementViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85625995269C953C00EE44BC /* PasswordManagementViewController.swift */; }; 3706FC7C293F65D500E42796 /* ImportedBookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CFA26FE191E001E4761 /* ImportedBookmarks.swift */; }; - 3706FC7D293F65D500E42796 /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */; }; 3706FC7E293F65D500E42796 /* MainWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7412B424D1536B00D22FE0 /* MainWindowController.swift */; }; 3706FC7F293F65D500E42796 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA9FF95824A1ECF20039E328 /* Tab.swift */; }; 3706FC81293F65D500E42796 /* DispatchQueueExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63D467025BFA6C100874977 /* DispatchQueueExtensions.swift */; }; @@ -1043,7 +1040,6 @@ 3707C71C294B5D1900682A9F /* TabExtensionsBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C00ED6292FB4B4009C73A6 /* TabExtensionsBuilder.swift */; }; 3707C71E294B5D2900682A9F /* URLRequestExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA88D14A252A557100980B4E /* URLRequestExtension.swift */; }; 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B7A60A0273E0BE400BBDFEB /* WKWebsiteDataStoreExtension.swift */; }; - 3707C721294B5D2900682A9F /* WKMenuItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */; }; 3707C722294B5D2900682A9F /* WKWebViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA92127625ADA07900600CD4 /* WKWebViewExtension.swift */; }; 3707C723294B5D2900682A9F /* URLSessionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */; }; 3707C724294B5D2900682A9F /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA8EDF2624923EC70071C2E8 /* StringExtension.swift */; }; @@ -1145,6 +1141,8 @@ 37479F152891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */; }; 374EF08329B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; 374EF08429B7575B003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; + 374EFDEB2D01A1D800B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEA2D01A1D800B30939 /* Utilities */; }; + 374EFDED2D01A1DE00B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEC2D01A1DE00B30939 /* Utilities */; }; 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534C9D28104D9B002621E7 /* TabLazyLoaderTests.swift */; }; 37534CA028113101002621E7 /* LazyLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534C9F28113101002621E7 /* LazyLoadable.swift */; }; 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA2281132CB002621E7 /* TabLazyLoaderDataSource.swift */; }; @@ -1185,6 +1183,8 @@ 376E2D2729428353001CD31B /* BrokenSiteReportingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E163B9293A56F400963C10 /* BrokenSiteReportingReferenceTests.swift */; }; 376E2D282942843D001CD31B /* privacy-reference-tests in Resources */ = {isa = PBXBuildFile; fileRef = 31E163BF293A581900963C10 /* privacy-reference-tests */; }; 37716D8029707E5D00A9FC6D /* FireproofingReferenceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310E79BE294A19A8007C49E8 /* FireproofingReferenceTests.swift */; }; + 3772BEDD2D019CE90019B9EF /* DefaultsFavoritesActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */; }; + 3772BEDE2D019CE90019B9EF /* DefaultsFavoritesActionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */; }; 3775912D29AAC72700E26367 /* SyncPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3775912C29AAC72700E26367 /* SyncPreferences.swift */; }; 3775912E29AAC72700E26367 /* SyncPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3775912C29AAC72700E26367 /* SyncPreferences.swift */; }; 3775913629AB9A1C00E26367 /* SyncManagementDialogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */; }; @@ -1194,8 +1194,6 @@ 3776583127F8325B009A6B35 /* AutofillPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */; }; 377D801C2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D801B2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; 377D801F2AB48191002AF251 /* FavoritesDisplayModeSyncHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D801B2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift */; }; - 377D8D642C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */; }; - 377D8D652C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */; }; 378205F62837CBA800D1D4AA /* SavedStateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F52837CBA800D1D4AA /* SavedStateMock.swift */; }; 378205F8283BC6A600D1D4AA /* StartupPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */; }; 378205FB283C277800D1D4AA /* MainMenuTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378205FA283C277800D1D4AA /* MainMenuTests.swift */; }; @@ -1214,8 +1212,6 @@ 3797C7A02C61806500DA77FB /* HomePageSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3797C79F2C61806500DA77FB /* HomePageSettingsView.swift */; }; 3797C7A32C62C38800DA77FB /* HomePageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3797C7A22C62C38800DA77FB /* HomePageSettingsModel.swift */; }; 3797C7A42C62C38800DA77FB /* HomePageSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3797C7A22C62C38800DA77FB /* HomePageSettingsModel.swift */; }; - 379B5AEF2CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */; }; - 379B5AF02CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */; }; 379DE4BD27EA31AC002CC3DE /* PreferencesAutofillView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */; }; 379E877629E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; 379E877729E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */; }; @@ -1301,15 +1297,11 @@ 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */; }; 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */; }; - 37F1E32B2CEF2DA800130142 /* NewTabPageFavoritesActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F1E32A2CEF2DA200130142 /* NewTabPageFavoritesActionsHandler.swift */; }; - 37F1E32C2CEF2DA800130142 /* NewTabPageFavoritesActionsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F1E32A2CEF2DA200130142 /* NewTabPageFavoritesActionsHandler.swift */; }; 37F1E32F2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */; }; 37F1E3302CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */; }; 37F44A5F298C17830025E7FE /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 37F44A5E298C17830025E7FE /* Navigation */; }; 37F8ABD32CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; 37F8ABD42CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; - 37F8E2362CEE3C01002F0141 /* NewTabPageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */; }; - 37F8E2372CEE3C01002F0141 /* NewTabPageFavoritesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */; }; 37FC2A182CF903080048E226 /* MockPrivacyStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */; }; 37FC2A192CF903080048E226 /* MockPrivacyStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */; }; 37FD78112A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; @@ -2308,7 +2300,6 @@ AA693E5E2696E5B90007BB78 /* CrashReports.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA693E5D2696E5B90007BB78 /* CrashReports.storyboard */; }; AA6AD95B2704B6DB00159F8A /* FirePopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6AD95A2704B6DB00159F8A /* FirePopoverViewController.swift */; }; AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6EF9AC25066F42004754E6 /* WindowsManager.swift */; }; - AA6EF9B3250785D5004754E6 /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */; }; AA6EF9B525081B4C004754E6 /* MainMenuActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6EF9B425081B4C004754E6 /* MainMenuActions.swift */; }; AA6FFB4424DC33320028F4D0 /* NSViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6FFB4324DC33320028F4D0 /* NSViewExtension.swift */; }; AA6FFB4624DC3B5A0028F4D0 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6FFB4524DC3B5A0028F4D0 /* WebView.swift */; }; @@ -2494,8 +2485,6 @@ B62A234129C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62A233F29C41D4400D22475 /* HistoryIntegrationTests.swift */; }; B62B48392ADE46FC000DECE5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62B48382ADE46FC000DECE5 /* Application.swift */; }; B62B483A2ADE46FC000DECE5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62B48382ADE46FC000DECE5 /* Application.swift */; }; - B62B483E2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62B483D2ADE48DE000DECE5 /* ArrayBuilder.swift */; }; - B62B483F2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62B483D2ADE48DE000DECE5 /* ArrayBuilder.swift */; }; B62B48562ADE730D000DECE5 /* FileImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62B48552ADE730D000DECE5 /* FileImportView.swift */; }; B62B48572ADE730D000DECE5 /* FileImportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62B48552ADE730D000DECE5 /* FileImportView.swift */; }; B62EB47C25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B62EB47B25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift */; }; @@ -2793,7 +2782,6 @@ B6DA06E22913AEDC00225DE2 /* TestNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E02913AEDB00225DE2 /* TestNavigationDelegate.swift */; }; B6DA06E42913ECEE00225DE2 /* ContextMenuManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E32913ECEE00225DE2 /* ContextMenuManager.swift */; }; B6DA06E62913F39400225DE2 /* MenuItemSelectors.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E52913F39400225DE2 /* MenuItemSelectors.swift */; }; - B6DA06E8291401D700225DE2 /* WKMenuItemIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */; }; B6DA44172616C13800DD1EC2 /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = B6DA44162616C13800DD1EC2 /* OHHTTPStubs */; }; B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B6DA44182616C13800DD1EC2 /* OHHTTPStubsSwift */; }; B6DA441E2616C84600DD1EC2 /* PixelStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA441D2616C84600DD1EC2 /* PixelStoreMock.swift */; }; @@ -3595,7 +3583,6 @@ 31A2FD162BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionFeatureGatekeeperTests.swift; sourceTree = ""; }; 31A83FB42BE28D7D00F74E67 /* UserText+DBP.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserText+DBP.swift"; sourceTree = ""; }; 31AA6B962B960B870025014E /* DataBrokerProtectionLoginItemPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionLoginItemPixels.swift; sourceTree = ""; }; - 31B4AF522901A4F20013585E /* NSEventExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 31C26A092CBE9D2200FFF462 /* PreferencesAIChat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesAIChat.swift; sourceTree = ""; }; 31C26A0C2CBE9DFA00FFF462 /* AIChatPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatPreferences.swift; sourceTree = ""; }; 31C3CE0128EDC1E70002C24A /* CustomRoundedCornersShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRoundedCornersShape.swift; sourceTree = ""; }; @@ -3696,13 +3683,14 @@ 376CC8B5296EBA8F006B63A7 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = ""; }; 376E708D2BD686260082B7EB /* UI Tests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "UI Tests.xctestplan"; sourceTree = ""; }; 37717E66296B5A20002FAEDF /* Global.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Global.xcconfig; sourceTree = ""; }; + 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsFavoritesActionHandler.swift; sourceTree = ""; }; + 3772BEDF2D01A1400019B9EF /* Utilities */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Utilities; sourceTree = ""; }; 3775912C29AAC72700E26367 /* SyncPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPreferences.swift; sourceTree = ""; }; 3775913529AB9A1C00E26367 /* SyncManagementDialogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncManagementDialogViewController.swift; sourceTree = ""; }; 3776582C27F71652009A6B35 /* WebsiteBreakageReportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteBreakageReportTests.swift; sourceTree = ""; }; 3776582E27F82E62009A6B35 /* AutofillPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillPreferences.swift; sourceTree = ""; }; 3776583027F8325B009A6B35 /* AutofillPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillPreferencesTests.swift; sourceTree = ""; }; 377D801B2AB47FBB002AF251 /* FavoritesDisplayModeSyncHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesDisplayModeSyncHandler.swift; sourceTree = ""; }; - 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAccessibilityProtocolExtension.swift; sourceTree = ""; }; 377E54382937B7C400780A0A /* DuckDuckGoAppStoreCI.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DuckDuckGoAppStoreCI.entitlements; sourceTree = ""; }; 378205F52837CBA800D1D4AA /* SavedStateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavedStateMock.swift; sourceTree = ""; }; 378205F7283BC6A600D1D4AA /* StartupPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupPreferencesTests.swift; sourceTree = ""; }; @@ -3724,7 +3712,6 @@ 379026D02C650CCD00A089E8 /* UserBackgroundImagesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBackgroundImagesManager.swift; sourceTree = ""; }; 3797C79F2C61806500DA77FB /* HomePageSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsView.swift; sourceTree = ""; }; 3797C7A22C62C38800DA77FB /* HomePageSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsModel.swift; sourceTree = ""; }; - 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesClient.swift; sourceTree = ""; }; 379DE4BC27EA31AC002CC3DE /* PreferencesAutofillView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesAutofillView.swift; sourceTree = ""; }; 379E877529E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCleanupErrorHandling.swift; sourceTree = ""; }; 37A089FA2C510FE0003BB417 /* RemoteMessagingDebugMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessagingDebugMenu.swift; sourceTree = ""; }; @@ -3787,10 +3774,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 = ""; }; - 37F1E32A2CEF2DA200130142 /* NewTabPageFavoritesActionsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesActionsHandler.swift; sourceTree = ""; }; 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesClientTests.swift; sourceTree = ""; }; 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverridesMenu.swift; sourceTree = ""; }; - 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesModel.swift; sourceTree = ""; }; 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyStats.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 = ""; }; @@ -4439,7 +4424,6 @@ AA693E5D2696E5B90007BB78 /* CrashReports.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = CrashReports.storyboard; sourceTree = ""; }; AA6AD95A2704B6DB00159F8A /* FirePopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverViewController.swift; sourceTree = ""; }; AA6EF9AC25066F42004754E6 /* WindowsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsManager.swift; sourceTree = ""; }; - AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = ""; }; AA6EF9B425081B4C004754E6 /* MainMenuActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenuActions.swift; sourceTree = ""; }; AA6FFB4324DC33320028F4D0 /* NSViewExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSViewExtension.swift; sourceTree = ""; }; AA6FFB4524DC3B5A0028F4D0 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; @@ -4587,7 +4571,6 @@ B62A233B29C322BC00D22475 /* NavigationProtectionIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationProtectionIntegrationTests.swift; sourceTree = ""; }; B62A233F29C41D4400D22475 /* HistoryIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryIntegrationTests.swift; sourceTree = ""; }; B62B48382ADE46FC000DECE5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; - B62B483D2ADE48DE000DECE5 /* ArrayBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayBuilder.swift; sourceTree = ""; }; B62B48552ADE730D000DECE5 /* FileImportView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileImportView.swift; sourceTree = ""; }; B62EB47B25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewPrivateMethodsAvailabilityTests.swift; sourceTree = ""; }; B630793926731F2600DCEE41 /* FileDownloadManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileDownloadManagerTests.swift; sourceTree = ""; }; @@ -4805,7 +4788,6 @@ B6DA06E02913AEDB00225DE2 /* TestNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestNavigationDelegate.swift; sourceTree = ""; }; B6DA06E32913ECEE00225DE2 /* ContextMenuManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuManager.swift; sourceTree = ""; }; B6DA06E52913F39400225DE2 /* MenuItemSelectors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemSelectors.swift; sourceTree = ""; }; - B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKMenuItemIdentifier.swift; sourceTree = ""; }; B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelDataStore.swift; sourceTree = ""; }; B6DA44072616B30600DD1EC2 /* PixelDataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = PixelDataModel.xcdatamodel; sourceTree = ""; }; B6DA441D2616C84600DD1EC2 /* PixelStoreMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelStoreMock.swift; sourceTree = ""; }; @@ -5028,6 +5010,7 @@ 9FF521482BAA909C00B9819B /* Lottie in Frameworks */, CBECDB8E2CDBD62C005B8B87 /* PageRefreshMonitor in Frameworks */, 984FD3BF299ACF35007334DD /* Bookmarks in Frameworks */, + 374EFDED2D01A1DE00B30939 /* Utilities in Frameworks */, 37A5E2F0298AA1B20047046B /* Persistence in Frameworks */, 9DC70B1A2AA1FA5B005A844B /* LoginItems in Frameworks */, 37269EFD2B332FAC005E8E46 /* Common in Frameworks */, @@ -5320,6 +5303,7 @@ 7B2366842C09FAC2002D393F /* VPNAppLauncher in Frameworks */, 85D44B862BA08D29001B4AB5 /* Suggestions in Frameworks */, 37DF000529F9C056002B7D3E /* SyncDataProviders in Frameworks */, + 374EFDEB2D01A1D800B30939 /* Utilities in Frameworks */, 37BA812D29B3CD690053F1A3 /* SyncUI in Frameworks */, 9D9DE5732C63AA0700D20B15 /* AppKitExtensions in Frameworks */, F1DF95E72BD188B60045E591 /* LoginItems in Frameworks */, @@ -5840,7 +5824,7 @@ 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */, 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */, 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */, - 37F1E32D2CEF2DDD00130142 /* Favorites */, + 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */, ); path = NewTabPage; sourceTree = ""; @@ -5978,6 +5962,7 @@ 1E862A882A9FC01200F84D4B /* SubscriptionUI */, 7BEC182D2AD5D89C00D30536 /* SystemExtensionManager */, 7B8594172B5B25FB0007EB3E /* UDSHelper */, + 3772BEDF2D01A1400019B9EF /* Utilities */, 3720B7F82D00DA4500D20F23 /* WebKitExtensions */, 7B76E6852AD5D77600186A84 /* XPCHelper */, ); @@ -6059,16 +6044,6 @@ path = PinnedTabs; sourceTree = ""; }; - 37F1E32D2CEF2DDD00130142 /* Favorites */ = { - isa = PBXGroup; - children = ( - 379B5AEE2CEA30EA00B9F5D7 /* NewTabPageFavoritesClient.swift */, - 37F8E2352CEE3BF5002F0141 /* NewTabPageFavoritesModel.swift */, - 37F1E32A2CEF2DA200130142 /* NewTabPageFavoritesActionsHandler.swift */, - ); - path = Favorites; - sourceTree = ""; - }; 37FC2A162CF903000048E226 /* Mocks */ = { isa = PBXGroup; children = ( @@ -6807,7 +6782,6 @@ isa = PBXGroup; children = ( 37534CA62811988E002621E7 /* AdjacentItemEnumerator.swift */, - B62B483D2ADE48DE000DECE5 /* ArrayBuilder.swift */, B6E319372953446000DD3BCF /* Assertions.swift */, 4BB6CE5E26B77ED000EC5860 /* Cryptography.swift */, 4BB88B5A25B7BA50006F6B06 /* Instruments.swift */, @@ -8833,7 +8807,6 @@ 0230C0A2272080090018F728 /* KeyedCodingExtension.swift */, 4B8D9061276D1D880078DB17 /* LocaleExtension.swift */, B66B9C5B29A5EBAD0010E8F3 /* NavigationActionExtension.swift */, - 377D8D632C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift */, 85308E24267FC9F2001ABD76 /* NSAlertExtension.swift */, F44C130125C2DA0400426E3E /* NSAppearanceExtension.swift */, AA5C8F622591021700748EB7 /* NSApplicationExtension.swift */, @@ -8842,13 +8815,11 @@ B63D467925BFC3E100874977 /* NSCoderExtensions.swift */, F41D174025CB131900472416 /* NSColorExtension.swift */, 843965112C6F2FFE004C8899 /* NSDragOperationExtension.swift */, - 31B4AF522901A4F20013585E /* NSEventExtension.swift */, B657841825FA484B00D8DB33 /* NSException+Catch.h */, B657841925FA484B00D8DB33 /* NSException+Catch.m */, B657841E25FA497600D8DB33 /* NSException+Catch.swift */, 4B139AFC26B60BD800894F82 /* NSImageExtensions.swift */, B6B71C572B23379600487131 /* NSLayoutConstraintExtension.swift */, - AA6EF9B2250785D5004754E6 /* NSMenuExtension.swift */, AA72D5FD25FFF94E00C77619 /* NSMenuItemExtension.swift */, 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */, B68412122B694BA10092F66A /* NSObject+performSelector.h */, @@ -8886,7 +8857,6 @@ B6DB3AEE278D5C370024C5C4 /* URLSessionExtension.swift */, B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */, B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */, - B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */, B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */, B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */, 84DDB9092C92B667008C997B /* WKVisitedLinkStoreWrapper.swift */, @@ -9980,6 +9950,7 @@ 37DF37062CF38B9F005ED34B /* PrivacyStats */, 371BBC582D00C897008FA0C7 /* NewTabPage */, 37EE81422D00DBCC0068034A /* WebKitExtensions */, + 374EFDEC2D01A1DE00B30939 /* Utilities */, ); productName = DuckDuckGo; productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */; @@ -10462,6 +10433,7 @@ 37DF37042CF38B96005ED34B /* PrivacyStats */, 371BBC562D00C891008FA0C7 /* NewTabPage */, 37EE81402D00DBC40068034A /* WebKitExtensions */, + 374EFDEA2D01A1D800B30939 /* Utilities */, ); productName = DuckDuckGo; productReference = AA585D7E248FD31100E9A3E2 /* DuckDuckGo.app */; @@ -11350,6 +11322,7 @@ 3706FABF293F65D500E42796 /* CrashReportReader.swift in Sources */, B6B4D1C62B0B3B5400C26286 /* DataImportReportModel.swift in Sources */, 3768D8452C2CC884004120AE /* RemoteMessagingConfigMatcherProvider.swift in Sources */, + 3772BEDD2D019CE90019B9EF /* DefaultsFavoritesActionHandler.swift in Sources */, 3706FAC0293F65D500E42796 /* DataTaskProviding.swift in Sources */, 3706FAC1293F65D500E42796 /* FeedbackViewController.swift in Sources */, 3706FAC2293F65D500E42796 /* FaviconSelector.swift in Sources */, @@ -11452,7 +11425,6 @@ 3706FAFD293F65D500E42796 /* DownloadsPopover.swift in Sources */, 3706FAFE293F65D500E42796 /* SpacerNode.swift in Sources */, 3706FB00293F65D500E42796 /* PasswordManagementCreditCardModel.swift in Sources */, - 3706FB01293F65D500E42796 /* NSEventExtension.swift in Sources */, 3706FB02293F65D500E42796 /* Onboarding.swift in Sources */, 3706FEB8293F6EFB00E42796 /* ConnectBitwardenView.swift in Sources */, 1DFAB51E2A8982A600A0F7F6 /* SetExtension.swift in Sources */, @@ -11481,7 +11453,6 @@ 3706FB15293F65D500E42796 /* NSNotificationName+DataImport.swift in Sources */, 37F8ABD42CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */, EE6666702B56EDE4001D898D /* VPNLocationsHostingViewController.swift in Sources */, - 37F8E2372CEE3C01002F0141 /* NewTabPageFavoritesModel.swift in Sources */, 3706FB16293F65D500E42796 /* StoredPermission.swift in Sources */, 3706FB17293F65D500E42796 /* FirePopoverCollectionViewHeader.swift in Sources */, 85774B042A71CDD000DE0561 /* BlockMenuItem.swift in Sources */, @@ -11647,7 +11618,6 @@ 37CBCA9B2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */, 370C230C2C76A3D600A80A3E /* BackgroundCategoryView.swift in Sources */, 370C230D2C76A3D600A80A3E /* BackgroundThumbnailView.swift in Sources */, - 37F1E32B2CEF2DA800130142 /* NewTabPageFavoritesActionsHandler.swift in Sources */, 370C230E2C76A3D600A80A3E /* BackgroundPickerView.swift in Sources */, 370C230F2C76A3D600A80A3E /* SettingsGrid.swift in Sources */, 3707C72A294B5D2900682A9F /* URLExtension.swift in Sources */, @@ -11830,7 +11800,6 @@ 314872782CC689AD00EEF89B /* AIChatToolBarPopUpOnboardingViewController.swift in Sources */, 3706FBD6293F65D500E42796 /* Instruments.swift in Sources */, B6ABD0CF2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */, - B62B483F2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, 569277C229DDCBB500B633EF /* HomePageContinueSetUpModel.swift in Sources */, 3767318F2C7F32E900EB097B /* SolidColorBackground.swift in Sources */, 3706FBD7293F65D500E42796 /* ContentBlockerRulesLists.swift in Sources */, @@ -11865,7 +11834,6 @@ 3706FBE3293F65D500E42796 /* WindowControllersManager.swift in Sources */, 37197EAA2942443D00394917 /* ModalSheetCancellable.swift in Sources */, 31267C6B2B640C5200FEF811 /* DataBrokerProtectionAppEvents.swift in Sources */, - 379B5AF02CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */, 3706FBE4293F65D500E42796 /* FireAnimationView.swift in Sources */, 3706FBE5293F65D500E42796 /* FaviconUrlReference.swift in Sources */, 3199AF802C80734A003AEBDC /* TabModalManageable.swift in Sources */, @@ -12057,7 +12025,6 @@ 9F514F922B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift in Sources */, 3706FC52293F65D500E42796 /* MouseOverAnimationButton.swift in Sources */, B60293E72BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, - 377D8D652C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */, 3706FC53293F65D500E42796 /* TabBarScrollView.swift in Sources */, B6104E9C2BA9C173008636B2 /* DownloadResumeData.swift in Sources */, 3706FC54293F65D500E42796 /* BookmarkListTreeControllerDataSource.swift in Sources */, @@ -12131,7 +12098,6 @@ 3706FC7B293F65D500E42796 /* PasswordManagementViewController.swift in Sources */, CD2AB5C02C8222F20019EB49 /* MaliciousSiteProtectionManager.swift in Sources */, 3706FC7C293F65D500E42796 /* ImportedBookmarks.swift in Sources */, - 3706FC7D293F65D500E42796 /* NSMenuExtension.swift in Sources */, 3701C9CF29BD040C00305B15 /* FirefoxBerkeleyDatabaseReader.swift in Sources */, B65E5DB02B74E6A900480415 /* TrackerNetwork.swift in Sources */, F17114862C7C9D28009836C1 /* Logger+Fire.swift in Sources */, @@ -12146,7 +12112,6 @@ 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 7B5A23702C46A116007213AC /* ExcludedDomainsModel.swift in Sources */, 9D9AE86E2AA76D1F0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, - 3707C721294B5D2900682A9F /* WKMenuItemIdentifier.swift in Sources */, 3706FEBE293F6EFF00E42796 /* BWMessageIdGenerator.swift in Sources */, C1E961F02B87AA29001760E1 /* AutofillActionBuilder.swift in Sources */, B6F1B0232BCE5658005E863C /* BrokenSiteInfoTabExtension.swift in Sources */, @@ -13031,7 +12996,6 @@ 567A23BE2C7F539C0010F66C /* SpecialErrorPageUserScriptExtension.swift in Sources */, AA5FA697275F90C400DCE9C9 /* FaviconImageCache.swift in Sources */, 1430DFF524D0580F00B8978C /* TabBarViewController.swift in Sources */, - B62B483E2ADE48DE000DECE5 /* ArrayBuilder.swift in Sources */, 4B92929B26670D2A00AD2C21 /* BookmarkOutlineViewDataSource.swift in Sources */, 56D145EB29E6C99B00E3488A /* DataImportStatusProviding.swift in Sources */, 843965122C6F2FFE004C8899 /* NSDragOperationExtension.swift in Sources */, @@ -13148,7 +13112,6 @@ 7B7F5D242C52725A00826256 /* AddExcludedDomainButtonsView.swift in Sources */, B6C0BB6729AEFF8100AE8E3C /* BookmarkExtension.swift in Sources */, 4BE6547F271FCD4D008D1D63 /* PasswordManagementCreditCardModel.swift in Sources */, - 31B4AF532901A4F20013585E /* NSEventExtension.swift in Sources */, 85707F26276A335700DC0649 /* Onboarding.swift in Sources */, B68C92C1274E3EF4002AC6B0 /* PopUpWindow.swift in Sources */, AA5FA6A0275F948900DCE9C9 /* Favicons.xcdatamodeld in Sources */, @@ -13275,7 +13238,6 @@ B63ED0E526BB8FB900A9DAD1 /* SharingMenu.swift in Sources */, AA4FF40C2624751A004E2377 /* GrammarFeaturesManager.swift in Sources */, 4B9DB0442A983B24000927DB /* WaitlistModalViewController.swift in Sources */, - B6DA06E8291401D700225DE2 /* WKMenuItemIdentifier.swift in Sources */, 4B0AACAE28BC6FD0001038AC /* SafariFaviconsReader.swift in Sources */, B60293E62BA19ECD0033186B /* NetPPopoverManagerMock.swift in Sources */, B6B3E0E12657EA7A0040E0A2 /* NSScreenExtension.swift in Sources */, @@ -13537,7 +13499,6 @@ F1B33DF22BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */, 370C22FE2C7698AE00A80A3E /* ThemePicker.swift in Sources */, F118EA852BEACC7000F77634 /* NonStandardPixel.swift in Sources */, - 377D8D642C105B8B001F5F6B /* NSAccessibilityProtocolExtension.swift in Sources */, 31CF74572CDC177D004ACCE5 /* AIChatUserScript.swift in Sources */, 1DB9618329F67F6200CF5568 /* FaviconNullStore.swift in Sources */, 8400DC4E2C6E2770006509D2 /* SteppedScrollView.swift in Sources */, @@ -13554,7 +13515,6 @@ 85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */, 371BBC5E2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift in Sources */, B6B4D1CA2B0C8C9200C26286 /* FirefoxCompatibilityPreferences.swift in Sources */, - 379B5AEF2CEA30F100B9F5D7 /* NewTabPageFavoritesClient.swift in Sources */, AA6197C4276B314D008396F0 /* FaviconUrlReference.swift in Sources */, B696AFFB2AC5924800C93203 /* FileLineError.swift in Sources */, 1D39E57A2C2C0F3700757339 /* ReleaseNotesUserScript.swift in Sources */, @@ -13566,6 +13526,7 @@ 4BE65479271FCD41008D1D63 /* EditableTextView.swift in Sources */, AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */, 1DA84D322C119AE70011C80F /* UpdateMenuItemFactory.swift in Sources */, + 3772BEDE2D019CE90019B9EF /* DefaultsFavoritesActionHandler.swift in Sources */, B6B4D1CF2B0E0DD000C26286 /* DataImportNoDataView.swift in Sources */, B688B4DA273E6D3B0087BEAF /* MainView.swift in Sources */, B61E2CD5294346C000773D8A /* Tab+Navigation.swift in Sources */, @@ -13587,7 +13548,6 @@ AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */, B64C853D26944B940048FEBE /* PermissionStore.swift in Sources */, AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */, - 37F8E2362CEE3C01002F0141 /* NewTabPageFavoritesModel.swift in Sources */, 4BB99D0126FE191E001E4761 /* ChromiumBookmarksReader.swift in Sources */, B6C0B23426E71BCD0031CB7F /* Downloads.xcdatamodeld in Sources */, 9FBD84732BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */, @@ -13686,7 +13646,6 @@ B693955726F04BEC0015B914 /* MouseOverButton.swift in Sources */, AA61C0D02722159B00E6B681 /* FireInfoViewController.swift in Sources */, C1C405872C7F80E50089DE8A /* PromotionView+FreemiumDBP.swift in Sources */, - 37F1E32C2CEF2DA800130142 /* NewTabPageFavoritesActionsHandler.swift in Sources */, 9D9AE8692AA76CDC0026E7DC /* LoginItem+NetworkProtection.swift in Sources */, 9F9C49F92BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */, @@ -13814,7 +13773,6 @@ B626A75A29921FAA00053070 /* NavigationActionPolicyExtension.swift in Sources */, 1DDD3EBC2B84DCB9004CBF2B /* WebTrackingProtectionPreferences.swift in Sources */, B603FD9E2A02712E00F3FCA9 /* CIImageExtension.swift in Sources */, - AA6EF9B3250785D5004754E6 /* NSMenuExtension.swift in Sources */, AA7412B524D1536B00D22FE0 /* MainWindowController.swift in Sources */, AA9FF95924A1ECF20039E328 /* Tab.swift in Sources */, 4BBDEE9228FC14760092FAA6 /* ConnectBitwardenView.swift in Sources */, @@ -15607,6 +15565,14 @@ isa = XCSwiftPackageProductDependency; productName = PreferencesViews; }; + 374EFDEA2D01A1D800B30939 /* Utilities */ = { + isa = XCSwiftPackageProductDependency; + productName = Utilities; + }; + 374EFDEC2D01A1DE00B30939 /* Utilities */ = { + isa = XCSwiftPackageProductDependency; + productName = Utilities; + }; 378F44E329B4BDE900899924 /* SwiftUIExtensions */ = { isa = XCSwiftPackageProductDependency; productName = SwiftUIExtensions; diff --git a/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift b/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift index f9acf0b9e0..3644c6446b 100644 --- a/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift @@ -17,6 +17,7 @@ // import AppKit +import Utilities typealias NSAttributedStringBuilder = ArrayBuilder extension NSAttributedString { diff --git a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift index 1cdf2941e9..f403d829be 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSMenuItemExtension.swift @@ -26,29 +26,6 @@ extension NSMenuItem { return item } - convenience init(title string: String, action selector: Selector? = nil, target: AnyObject? = nil, keyEquivalent: NSEvent.KeyEquivalent = [], representedObject: Any? = nil, state: NSControl.StateValue = .off, items: [NSMenuItem]? = nil) { - self.init(title: string, action: selector, keyEquivalent: keyEquivalent.charCode) - if !keyEquivalent.modifierMask.isEmpty { - self.keyEquivalentModifierMask = keyEquivalent.modifierMask - } - self.target = target - self.representedObject = representedObject - self.state = state - - if let items { - self.submenu = NSMenu(title: title, items: items) - } - } - - convenience init(title string: String, action selector: Selector? = nil, target: AnyObject? = nil, keyEquivalent: NSEvent.KeyEquivalent = [], representedObject: Any? = nil, state: NSControl.StateValue = .off, @MenuBuilder items: () -> [NSMenuItem]) { - self.init(title: string, action: selector, target: target, keyEquivalent: keyEquivalent, representedObject: representedObject, state: state, items: items()) - } - - convenience init(action selector: Selector?) { - self.init() - self.action = selector - } - convenience init(bookmarkViewModel: BookmarkViewModel) { self.init() @@ -65,67 +42,4 @@ extension NSMenuItem { representedObject = bookmarkViewModels action = #selector(MainViewController.openAllInTabs(_:)) } - - convenience init(title: String) { - self.init(title: title, action: nil, keyEquivalent: "") - } - - var topMenu: NSMenu? { - var menuItem = self - while let parent = menuItem.parent { - menuItem = parent - } - - return menuItem.menu - } - - func removeFromParent() { - parent?.submenu?.removeItem(self) - } - - @discardableResult - func alternate() -> NSMenuItem { - self.isAlternate = true - return self - } - - @discardableResult - func hidden() -> NSMenuItem { - self.isHidden = true - if !keyEquivalent.isEmpty { - self.allowsKeyEquivalentWhenHidden = true - } - return self - } - - @discardableResult - func submenu(_ submenu: NSMenu) -> NSMenuItem { - self.submenu = submenu - return self - } - - @discardableResult - func withImage(_ image: NSImage?) -> NSMenuItem { - self.image = image - return self - } - - @discardableResult - func targetting(_ target: AnyObject) -> NSMenuItem { - self.target = target - return self - } - - @discardableResult - func withSubmenu(_ submenu: NSMenu) -> NSMenuItem { - self.submenu = submenu - return self - } - - @discardableResult - func withModifierMask(_ mask: NSEvent.ModifierFlags) -> NSMenuItem { - self.keyEquivalentModifierMask = mask - return self - } - } diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index a2257eaa54..a49d39f36d 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -144,11 +144,6 @@ extension URL { // base url for Error Page Alternate HTML loaded into Web View static let error = URL(string: "duck://error")! - static func duckFavicon(for faviconURL: URL) -> URL? { - let encodedURL = faviconURL.absoluteString.percentEncoded(withAllowedCharacters: .urlPathAllowed) - return URL(string: "duck://favicon/\(encodedURL)") - } - static let dataBrokerProtection = URL(string: "duck://personal-information-removal")! #if !SANDBOX_TEST_TOOL diff --git a/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift b/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift index 50cbe4ea7e..da95c2d4f2 100644 --- a/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/NSPopUpButtonView.swift @@ -18,6 +18,7 @@ import AppKit import SwiftUI +import Utilities struct PopupButtonItem: Equatable { diff --git a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift b/DuckDuckGo/NewTabPage/DefaultsFavoritesActionHandler.swift similarity index 79% rename from DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift rename to DuckDuckGo/NewTabPage/DefaultsFavoritesActionHandler.swift index 6a73fb3e93..207904a3eb 100644 --- a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift +++ b/DuckDuckGo/NewTabPage/DefaultsFavoritesActionHandler.swift @@ -1,5 +1,5 @@ // -// NewTabPageFavoritesActionsHandler.swift +// DefaultsFavoritesActionHandler.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -16,21 +16,12 @@ // limitations under the License. // -import Foundation +import Combine import NewTabPage -protocol FavoritesActionsHandling { - @MainActor func open(_ url: URL, target: NewTabPageFavoritesModel.OpenTarget) - @MainActor func addNewFavorite() - @MainActor func edit(_ bookmark: Bookmark) - @MainActor func onFaviconMissing() - - func removeFavorite(_ bookmark: Bookmark) - func deleteBookmark(_ bookmark: Bookmark) - func move(_ bookmarkID: String, toIndex: Int) -} - final class DefaultFavoritesActionsHandler: FavoritesActionsHandling { + typealias Favorite = Bookmark + let bookmarkManager: BookmarkManager init(bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { @@ -38,7 +29,7 @@ final class DefaultFavoritesActionsHandler: FavoritesActionsHandling { } @MainActor - func open(_ url: URL, target: NewTabPageFavoritesModel.OpenTarget) { + func open(_ url: URL, target: FavoriteOpenTarget) { guard let tabCollectionViewModel else { return } @@ -56,13 +47,13 @@ final class DefaultFavoritesActionsHandler: FavoritesActionsHandling { } } - func removeFavorite(_ bookmark: Bookmark) { - bookmark.isFavorite = false - bookmarkManager.update(bookmark: bookmark) + func removeFavorite(_ favorite: Bookmark) { + favorite.isFavorite = false + bookmarkManager.update(bookmark: favorite) } - func deleteBookmark(_ bookmark: Bookmark) { - bookmarkManager.remove(bookmark: bookmark, undoManager: nil) + func deleteBookmark(for favorite: Bookmark) { + bookmarkManager.remove(bookmark: favorite, undoManager: nil) } @MainActor @@ -72,9 +63,9 @@ final class DefaultFavoritesActionsHandler: FavoritesActionsHandling { } @MainActor - func edit(_ bookmark: Bookmark) { + func edit(_ favorite: Bookmark) { guard let window else { return } - BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: bookmark).show(in: window) + BookmarksDialogViewFactory.makeEditBookmarkView(bookmark: favorite).show(in: window) } func move(_ bookmarkID: String, toIndex index: Int) { @@ -104,3 +95,5 @@ final class DefaultFavoritesActionsHandler: FavoritesActionsHandling { return .init(syncService: syncService, syncBookmarksAdapter: syncBookmarksAdapter) }() } + +extension Bookmark: NewTabPageFavorite {} diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index d0d0c1a5b7..e52873c650 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -35,11 +35,14 @@ extension NewTabPageActionsManager { getLegacyIsViewExpandedSetting: UserDefaultsWrapper(key: .homePageShowRecentlyVisited, defaultValue: false).wrappedValue ) + let favoritesPublisher = LocalBookmarkManager.shared.listPublisher.map({ $0?.favoriteBookmarks ?? [] }).eraseToAnyPublisher() + let favoritesModel = NewTabPageFavoritesModel(actionsHandler: DefaultFavoritesActionsHandler(), favoritesPublisher: favoritesPublisher) + self.init(scriptClients: [ NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel, openURLHandler: openURLHandler), NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), - NewTabPageFavoritesClient(favoritesModel: NewTabPageFavoritesModel()), + NewTabPageFavoritesClient(favoritesModel: favoritesModel), NewTabPagePrivacyStatsClient(model: privacyStatsModel) ]) } diff --git a/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift b/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift index 57174a6ec7..d9c624ccf4 100644 --- a/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift +++ b/DuckDuckGo/Tab/TabExtensions/ContextMenuManager.swift @@ -19,7 +19,7 @@ import AppKit import Combine import Foundation -import WebKit +import WebKitExtensions enum NavigationDecision { case allow(NewWindowPolicy) diff --git a/LocalPackages/AppKitExtensions/Package.swift b/LocalPackages/AppKitExtensions/Package.swift index 9f00f940c3..ddca2d2f6d 100644 --- a/LocalPackages/AppKitExtensions/Package.swift +++ b/LocalPackages/AppKitExtensions/Package.swift @@ -10,11 +10,13 @@ let package = Package( .library(name: "AppKitExtensions", targets: ["AppKitExtensions"]), ], dependencies: [ + .package(path: "../Utilities"), ], targets: [ .target( name: "AppKitExtensions", dependencies: [ + "Utilities" ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)) diff --git a/DuckDuckGo/Common/Extensions/NSAccessibilityProtocolExtension.swift b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSAccessibilityProtocolExtension.swift similarity index 96% rename from DuckDuckGo/Common/Extensions/NSAccessibilityProtocolExtension.swift rename to LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSAccessibilityProtocolExtension.swift index c4ed30119a..3cffbe9f08 100644 --- a/DuckDuckGo/Common/Extensions/NSAccessibilityProtocolExtension.swift +++ b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSAccessibilityProtocolExtension.swift @@ -18,7 +18,7 @@ import Foundation -extension NSAccessibilityProtocol { +public extension NSAccessibilityProtocol { @discardableResult func withAccessibilityIdentifier(_ accessibilityIdentifier: String) -> Self { diff --git a/DuckDuckGo/Common/Extensions/NSEventExtension.swift b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSEventExtension.swift similarity index 82% rename from DuckDuckGo/Common/Extensions/NSEventExtension.swift rename to LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSEventExtension.swift index cfbd091cd2..dbb30682d1 100644 --- a/DuckDuckGo/Common/Extensions/NSEventExtension.swift +++ b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSEventExtension.swift @@ -19,13 +19,17 @@ import AppKit import Combine -extension NSEvent { +public extension NSEvent { struct EventMonitorType: OptionSet { - let rawValue: UInt8 + public let rawValue: UInt8 - static let local = EventMonitorType(rawValue: 1 << 0) - static let global = EventMonitorType(rawValue: 1 << 1) + public init(rawValue: UInt8) { + self.rawValue = rawValue + } + + public static let local = EventMonitorType(rawValue: 1 << 0) + public static let global = EventMonitorType(rawValue: 1 << 1) } var deviceIndependentFlags: NSEvent.ModifierFlags { @@ -91,7 +95,7 @@ extension NSEvent { } -enum KeyEquivalentElement: ExpressibleByStringLiteral, Hashable { +public enum KeyEquivalentElement: ExpressibleByStringLiteral, Hashable { public typealias StringLiteralType = String case charCode(String) @@ -100,13 +104,13 @@ enum KeyEquivalentElement: ExpressibleByStringLiteral, Hashable { case option case control - static let backspace = KeyEquivalentElement.charCode("\u{8}") - static let tab = KeyEquivalentElement.charCode("\t") - static let left = KeyEquivalentElement.charCode("\u{2190}") - static let right = KeyEquivalentElement.charCode("\u{2192}") - static let escape = KeyEquivalentElement.charCode("\u{1B}") + public static let backspace = KeyEquivalentElement.charCode("\u{8}") + public static let tab = KeyEquivalentElement.charCode("\t") + public static let left = KeyEquivalentElement.charCode("\u{2190}") + public static let right = KeyEquivalentElement.charCode("\u{2192}") + public static let escape = KeyEquivalentElement.charCode("\u{1B}") - init(stringLiteral value: String) { + public init(stringLiteral value: String) { self = .charCode(value) } } @@ -114,11 +118,11 @@ enum KeyEquivalentElement: ExpressibleByStringLiteral, Hashable { extension NSEvent.KeyEquivalent: ExpressibleByStringLiteral, ExpressibleByUnicodeScalarLiteral, ExpressibleByExtendedGraphemeClusterLiteral { public typealias StringLiteralType = String - static let backspace: Self = [.backspace] - static let tab: Self = [.tab] - static let left: Self = [.left] - static let right: Self = [.right] - static let escape: Self = [.escape] + public static let backspace: Self = [.backspace] + public static let tab: Self = [.tab] + public static let left: Self = [.left] + public static let right: Self = [.right] + public static let escape: Self = [.escape] public init(stringLiteral value: String) { self = [.charCode(value)] @@ -145,7 +149,7 @@ extension NSEvent.KeyEquivalent: ExpressibleByStringLiteral, ExpressibleByUnicod } } - var charCode: String { + public var charCode: String { for item in self { if case .charCode(let value) = item { return value @@ -154,7 +158,7 @@ extension NSEvent.KeyEquivalent: ExpressibleByStringLiteral, ExpressibleByUnicod return "" } - var modifierMask: NSEvent.ModifierFlags { + public var modifierMask: NSEvent.ModifierFlags { var result: NSEvent.ModifierFlags = [] for item in self { switch item { diff --git a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuExtension.swift similarity index 92% rename from DuckDuckGo/Common/Extensions/NSMenuExtension.swift rename to LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuExtension.swift index 6790d5c4f7..d7350e4e07 100644 --- a/DuckDuckGo/Common/Extensions/NSMenuExtension.swift +++ b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuExtension.swift @@ -17,10 +17,11 @@ // import AppKit +import Utilities typealias MenuBuilder = ArrayBuilder -extension NSMenu { +public extension NSMenu { convenience init(title: String = "", items: [NSMenuItem]) { self.init(title: title) @@ -41,10 +42,6 @@ extension NSMenu { return items.enumerated().first(where: { $0.element.identifier?.rawValue == id })?.offset } - func item(with identifier: WKMenuItemIdentifier) -> NSMenuItem? { - return indexOfItem(withIdentifier: identifier.rawValue).map { self.items[$0] } - } - func indexOfItem(with action: Selector) -> Int? { return items.enumerated().first(where: { $0.element.action == action })?.offset } diff --git a/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuItemExtension.swift b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuItemExtension.swift new file mode 100644 index 0000000000..cad7b1f34e --- /dev/null +++ b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSMenuItemExtension.swift @@ -0,0 +1,108 @@ +// +// NSMenuItemExtension.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 + +public extension NSMenuItem { + + convenience init(title string: String, action selector: Selector? = nil, target: AnyObject? = nil, keyEquivalent: NSEvent.KeyEquivalent = [], representedObject: Any? = nil, state: NSControl.StateValue = .off, items: [NSMenuItem]? = nil) { + self.init(title: string, action: selector, keyEquivalent: keyEquivalent.charCode) + if !keyEquivalent.modifierMask.isEmpty { + self.keyEquivalentModifierMask = keyEquivalent.modifierMask + } + self.target = target + self.representedObject = representedObject + self.state = state + + if let items { + self.submenu = NSMenu(title: title, items: items) + } + } + + convenience init(title string: String, action selector: Selector? = nil, target: AnyObject? = nil, keyEquivalent: NSEvent.KeyEquivalent = [], representedObject: Any? = nil, state: NSControl.StateValue = .off, @MenuBuilder items: () -> [NSMenuItem]) { + self.init(title: string, action: selector, target: target, keyEquivalent: keyEquivalent, representedObject: representedObject, state: state, items: items()) + } + + convenience init(action selector: Selector?) { + self.init() + self.action = selector + } + + convenience init(title: String) { + self.init(title: title, action: nil, keyEquivalent: "") + } + + var topMenu: NSMenu? { + var menuItem = self + while let parent = menuItem.parent { + menuItem = parent + } + + return menuItem.menu + } + + func removeFromParent() { + parent?.submenu?.removeItem(self) + } + + @discardableResult + func alternate() -> NSMenuItem { + self.isAlternate = true + return self + } + + @discardableResult + func hidden() -> NSMenuItem { + self.isHidden = true + if !keyEquivalent.isEmpty { + self.allowsKeyEquivalentWhenHidden = true + } + return self + } + + @discardableResult + func submenu(_ submenu: NSMenu) -> NSMenuItem { + self.submenu = submenu + return self + } + + @discardableResult + func withImage(_ image: NSImage?) -> NSMenuItem { + self.image = image + return self + } + + @discardableResult + func targetting(_ target: AnyObject) -> NSMenuItem { + self.target = target + return self + } + + @discardableResult + func withSubmenu(_ submenu: NSMenu) -> NSMenuItem { + self.submenu = submenu + return self + } + + @discardableResult + func withModifierMask(_ mask: NSEvent.ModifierFlags) -> NSMenuItem { + self.keyEquivalentModifierMask = mask + return self + } + +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift new file mode 100644 index 0000000000..4a2faf672d --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesActionsHandler.swift @@ -0,0 +1,39 @@ +// +// NewTabPageFavoritesActionsHandler.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 + +public protocol NewTabPageFavorite { + var id: String { get } + var title: String { get } + var url: String { get } + var urlObject: URL? { get } +} + +public protocol FavoritesActionsHandling { + associatedtype FavoriteType: NewTabPageFavorite + + @MainActor func open(_ url: URL, target: FavoriteOpenTarget) + @MainActor func addNewFavorite() + @MainActor func edit(_ favorite: FavoriteType) + @MainActor func onFaviconMissing() + + func removeFavorite(_ favorite: FavoriteType) + func deleteBookmark(for favorite: FavoriteType) + func move(_ favoriteID: String, toIndex: Int) +} diff --git a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift similarity index 59% rename from DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift index 1d2ba28728..282a5133c3 100644 --- a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift @@ -19,16 +19,18 @@ import Bookmarks import Common import Combine -import NewTabPage import UserScript +import WebKit -final class NewTabPageFavoritesClient: NewTabPageScriptClient { +public final class NewTabPageFavoritesClient: NewTabPageScriptClient where FavoriteType: NewTabPageFavorite, + ActionHandler: FavoritesActionsHandling, + ActionHandler.FavoriteType == FavoriteType { - let favoritesModel: NewTabPageFavoritesModel - weak var userScriptsSource: NewTabPageUserScriptsSource? + public let favoritesModel: NewTabPageFavoritesModel + public weak var userScriptsSource: NewTabPageUserScriptsSource? private var cancellables: Set = [] - init(favoritesModel: NewTabPageFavoritesModel) { + public init(favoritesModel: NewTabPageFavoritesModel) { self.favoritesModel = favoritesModel favoritesModel.$favorites.dropFirst() @@ -48,7 +50,7 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { .store(in: &cancellables) } - enum MessageName: String, CaseIterable { + public enum MessageName: String, CaseIterable { case add = "favorites_add" case getConfig = "favorites_getConfig" case getData = "favorites_getData" @@ -60,7 +62,7 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { case setConfig = "favorites_setConfig" } - func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + public func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { userScript.registerMessageHandlers([ MessageName.add.rawValue: { [weak self] in try await self?.add(params: $0, original: $1) }, MessageName.getConfig.rawValue: { [weak self] in try await self?.getConfig(params: $0, original: $1) }, @@ -72,18 +74,18 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { ]) } - func add(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func add(params: Any, original: WKScriptMessage) async throws -> Encodable? { await favoritesModel.addNew() return nil } - func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = favoritesModel.isViewExpanded ? .expanded : .collapsed return NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) } @MainActor - func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { return nil } @@ -92,17 +94,17 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { } @MainActor - func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let favorites = favoritesModel.favorites.map { - NewTabPageFavoritesClient.Favorite($0, favoritesModel: favoritesModel) + NewTabPageFavoritesClient.Favorite($0, onFaviconMissing: favoritesModel.onFaviconMissing) } return NewTabPageFavoritesClient.FavoritesData(favorites: favorites) } @MainActor - private func notifyDataUpdated(_ favorites: [Bookmark]) { + private func notifyDataUpdated(_ favorites: [NewTabPageFavorite]) { let favorites = favoritesModel.favorites.map { - NewTabPageFavoritesClient.Favorite($0, favoritesModel: favoritesModel) + NewTabPageFavoritesClient.Favorite($0, onFaviconMissing: favoritesModel.onFaviconMissing) } pushMessage(named: MessageName.onDataUpdate.rawValue, params: NewTabPageFavoritesClient.FavoritesData(favorites: favorites)) } @@ -115,7 +117,7 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { } @MainActor - func move(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func move(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let action: NewTabPageFavoritesClient.FavoritesMoveAction = DecodableHelper.decode(from: params) else { return nil } @@ -124,7 +126,7 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { } @MainActor - func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let action: NewTabPageFavoritesClient.FavoritesOpenAction = DecodableHelper.decode(from: params) else { return nil } @@ -133,7 +135,7 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { } @MainActor - func openContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { + public func openContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let contextMenuAction: NewTabPageFavoritesClient.FavoritesContextMenuAction = DecodableHelper.decode(from: params) else { return nil } @@ -142,65 +144,101 @@ final class NewTabPageFavoritesClient: NewTabPageScriptClient { } } -extension NewTabPageFavoritesClient { +public extension NewTabPageFavoritesClient { struct FavoritesContextMenuAction: Codable { - let id: String + public let id: String + + public init(id: String) { + self.id = id + } } struct FavoritesOpenAction: Codable { - let id: String - let url: String + public let id: String + public let url: String + + public init(id: String, url: String) { + self.id = id + self.url = url + } } struct FavoritesMoveAction: Codable { - let id: String - let fromIndex: Int - let targetIndex: Int + public let id: String + public let fromIndex: Int + public let targetIndex: Int + + public init(id: String, fromIndex: Int, targetIndex: Int) { + self.id = id + self.fromIndex = fromIndex + self.targetIndex = targetIndex + } } struct FavoritesConfig: Codable { - let expansion: Expansion + public let expansion: Expansion + + public init(expansion: Expansion) { + self.expansion = expansion + } - enum Expansion: String, Codable { + public enum Expansion: String, Codable { case expanded, collapsed } } struct FavoritesData: Encodable { - let favorites: [Favorite] + public let favorites: [Favorite] + + public init(favorites: [Favorite]) { + self.favorites = favorites + } } struct Favorite: Encodable, Equatable { - let favicon: FavoriteFavicon? - let id: String - let title: String - let url: String + public let favicon: FavoriteFavicon? + public let id: String + public let title: String + public let url: String + + public init(id: String, title: String, url: String, favicon: NewTabPageFavoritesClient.FavoriteFavicon? = nil) { + self.id = id + self.title = title + self.url = url + self.favicon = favicon + } @MainActor - init(_ bookmark: Bookmark, favoritesModel: NewTabPageFavoritesModel) { + init(_ bookmark: NewTabPageFavorite, onFaviconMissing: () -> Void) { id = bookmark.id title = bookmark.title url = bookmark.url - if let url = bookmark.url.url, let duckFaviconURL = URL.duckFavicon(for: url) { - favicon = FavoriteFavicon(maxAvailableSize: Int(Favicon.SizeCategory.medium.rawValue), src: duckFaviconURL.absoluteString) + if let url = bookmark.urlObject, let duckFaviconURL = URL.duckFavicon(for: url) { + // TODO: Int(Favicon.SizeCategory.medium.rawValue) + favicon = FavoriteFavicon(maxAvailableSize: 132, src: duckFaviconURL.absoluteString) } else { - favoritesModel.onFaviconMissing() + onFaviconMissing() favicon = nil } } + } - init(id: String, title: String, url: String, favicon: NewTabPageFavoritesClient.FavoriteFavicon? = nil) { - self.id = id - self.title = title - self.url = url - self.favicon = favicon + struct FavoriteFavicon: Encodable, Equatable { + public let maxAvailableSize: Int + public let src: String + + public init(maxAvailableSize: Int, src: String) { + self.maxAvailableSize = maxAvailableSize + self.src = src } } +} - struct FavoriteFavicon: Encodable, Equatable { - let maxAvailableSize: Int - let src: String +extension URL { + static func duckFavicon(for faviconURL: URL) -> URL? { + let encodedURL = faviconURL.absoluteString.percentEncoded(withAllowedCharacters: .urlPathAllowed) + return URL(string: "duck://favicon/\(encodedURL)") } } diff --git a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift similarity index 64% rename from DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift index 226594c22b..04056b5768 100644 --- a/DuckDuckGo/NewTabPage/Favorites/NewTabPageFavoritesModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift @@ -16,54 +16,57 @@ // limitations under the License. // +import AppKitExtensions import Combine import Foundation -import NewTabPage import Persistence -protocol NewTabPageFavoritesSettingsPersistor: AnyObject { +public protocol NewTabPageFavoritesSettingsPersistor: AnyObject { var isViewExpanded: Bool { get set } } -final class UserDefaultsNewTabPageFavoritesSettingsPersistor: NewTabPageFavoritesSettingsPersistor { +public final class UserDefaultsNewTabPageFavoritesSettingsPersistor: NewTabPageFavoritesSettingsPersistor { enum Keys { static let isViewExpanded = "new-tab-page.favorites.is-view-expanded" } private let keyValueStore: KeyValueStoring - init(_ keyValueStore: KeyValueStoring = UserDefaults.standard) { + public init(_ keyValueStore: KeyValueStoring = UserDefaults.standard) { self.keyValueStore = keyValueStore migrateFromNativeHomePageSettings() } - var isViewExpanded: Bool { + public var isViewExpanded: Bool { get { return keyValueStore.object(forKey: Keys.isViewExpanded) as? Bool ?? false } set { keyValueStore.set(newValue, forKey: Keys.isViewExpanded) } } private func migrateFromNativeHomePageSettings() { - guard keyValueStore.object(forKey: Keys.isViewExpanded) == nil else { - return - } - let legacyKey = UserDefaultsWrapper.Key.homePageShowAllFavorites.rawValue - isViewExpanded = keyValueStore.object(forKey: legacyKey) as? Bool ?? false +// guard keyValueStore.object(forKey: Keys.isViewExpanded) == nil else { +// return +// } +// let legacyKey = UserDefaultsWrapper.Key.homePageShowAllFavorites.rawValue +// isViewExpanded = keyValueStore.object(forKey: legacyKey) as? Bool ?? false } } -final class NewTabPageFavoritesModel: NSObject { +public enum FavoriteOpenTarget { + case current, newTab, newWindow +} - enum OpenTarget { - case current, newTab, newWindow - } +public final class NewTabPageFavoritesModel: NSObject where FavoriteType: NewTabPageFavorite, + ActionHandler: FavoritesActionsHandling, + ActionHandler.FavoriteType == FavoriteType { - private let actionsHandler: FavoritesActionsHandling + private let actionsHandler: ActionHandler private let contextMenuPresenter: NewTabPageContextMenuPresenting private let settingsPersistor: NewTabPageFavoritesSettingsPersistor private var cancellables: Set = [] - init( - actionsHandler: FavoritesActionsHandling, + public init( + actionsHandler: ActionHandler, + favoritesPublisher: AnyPublisher<[FavoriteType], Never>, contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), settingsPersistor: NewTabPageFavoritesSettingsPersistor = UserDefaultsNewTabPageFavoritesSettingsPersistor() ) { @@ -72,62 +75,52 @@ final class NewTabPageFavoritesModel: NSObject { self.settingsPersistor = settingsPersistor isViewExpanded = settingsPersistor.isViewExpanded - } - convenience init( - bookmarkManager: BookmarkManager = LocalBookmarkManager.shared, - contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), - settingsPersistor: NewTabPageFavoritesSettingsPersistor = UserDefaultsNewTabPageFavoritesSettingsPersistor() - ) { - self.init( - actionsHandler: DefaultFavoritesActionsHandler(bookmarkManager: bookmarkManager), - contextMenuPresenter: contextMenuPresenter, - settingsPersistor: settingsPersistor - ) + super.init() - bookmarkManager.listPublisher + favoritesPublisher .receive(on: DispatchQueue.main) - .sink { [weak self] list in - self?.favorites = list?.favoriteBookmarks ?? [] + .sink { [weak self] favorites in + self?.favorites = favorites } .store(in: &cancellables) } - @Published var isViewExpanded: Bool { + @Published public var isViewExpanded: Bool { didSet { settingsPersistor.isViewExpanded = self.isViewExpanded } } - @Published var favorites: [Bookmark] = [] + @Published public var favorites: [FavoriteType] = [] // MARK: - Actions @MainActor - func openFavorite(withURL url: String) { - guard let url = url.url else { return } + public func openFavorite(withURL url: String) { + guard let url = URL(string: url) else { return } actionsHandler.open(url, target: .current) } - func moveFavorite(withID bookmarkID: String, fromIndex: Int, toIndex index: Int) { + public func moveFavorite(withID bookmarkID: String, fromIndex: Int, toIndex index: Int) { let targetIndex = index > fromIndex ? index + 1 : index actionsHandler.move(bookmarkID, toIndex: targetIndex) } @MainActor - func addNew() { + public func addNew() { actionsHandler.addNewFavorite() } @MainActor - func onFaviconMissing() { + public func onFaviconMissing() { actionsHandler.onFaviconMissing() } // MARK: Context Menu @MainActor - func showContextMenu(for bookmarkID: String) { + public func showContextMenu(for bookmarkID: String) { /** * This isn't very effective (may need to traverse up to entire array) * but it's only ever needed for context menus. I decided to skip @@ -158,32 +151,32 @@ final class NewTabPageFavoritesModel: NSObject { } @MainActor - @objc func openInNewTab(_ sender: NSMenuItem) { + @objc public func openInNewTab(_ sender: NSMenuItem) { guard let url = sender.representedObject as? URL else { return } actionsHandler.open(url, target: .newTab) } @MainActor - @objc func openInNewWindow(_ sender: NSMenuItem) { + @objc public func openInNewWindow(_ sender: NSMenuItem) { guard let url = sender.representedObject as? URL else { return } actionsHandler.open(url, target: .newWindow) } @MainActor - @objc func editBookmark(_ sender: NSMenuItem) { - guard let bookmark = sender.representedObject as? Bookmark else { return } + @objc public func editBookmark(_ sender: NSMenuItem) { + guard let bookmark = sender.representedObject as? FavoriteType else { return } actionsHandler.edit(bookmark) } @MainActor - @objc func removeFavorite(_ sender: NSMenuItem) { - guard let bookmark = sender.representedObject as? Bookmark else { return } - actionsHandler.removeFavorite(bookmark) + @objc public func removeFavorite(_ sender: NSMenuItem) { + guard let favorite = sender.representedObject as? FavoriteType else { return } + actionsHandler.removeFavorite(favorite) } @MainActor - @objc func deleteBookmark(_ sender: NSMenuItem) { - guard let bookmark = sender.representedObject as? Bookmark else { return } - actionsHandler.deleteBookmark(bookmark) + @objc public func deleteBookmark(_ sender: NSMenuItem) { + guard let bookmark = sender.representedObject as? FavoriteType else { return } + actionsHandler.deleteBookmark(for: bookmark) } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/internal/UserText.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/internal/UserText.swift new file mode 100644 index 0000000000..833378e564 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/internal/UserText.swift @@ -0,0 +1,27 @@ +// +// UserText.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 + +enum UserText { + static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") + static let openInNewWindow = NSLocalizedString("open.in.new.window", value: "Open in New Window", comment: "Menu item that opens the link in a new window") + static let edit = NSLocalizedString("edit", value: "Edit", comment: "Edit button") + static let deleteBookmark = NSLocalizedString("delete-bookmark", value: "Delete Bookmark", comment: "Delete Bookmark button") + static let removeFavorite = NSLocalizedString("remove-favorite", value: "Remove Favorite", comment: "Remove Favorite button") +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift new file mode 100644 index 0000000000..c4e965eb90 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift @@ -0,0 +1,44 @@ +// +// DuckFaviconURLTests.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 XCTest +import Combine +@testable import NewTabPage + +final class DuckFaviconURLTests: XCTestCase { + + + func testDuckFaviconURL() throws { + XCTAssertEqual( + URL.duckFavicon(for: URL(string: "https://example.com")!)?.absoluteString, + "duck://favicon/https%3A//example.com" + ) + XCTAssertEqual( + URL.duckFavicon(for: URL(string: "https://example.com/1/2/3#anchor")!)?.absoluteString, + "duck://favicon/https%3A//example.com/1/2/3%23anchor" + ) + XCTAssertEqual( + URL.duckFavicon(for: URL(string: "https://example.com/1/2/3?query=yes&other=no")!)?.absoluteString, + "duck://favicon/https%3A//example.com/1/2/3%3Fquery=yes&other=no" + ) + XCTAssertEqual( + URL.duckFavicon(for: URL(string: "https://рнидс.срб/")!)?.absoluteString, + "duck://favicon/https%3A//xn--d1aholi.xn--90a3ac/" + ) + } +} diff --git a/LocalPackages/Utilities/.gitignore b/LocalPackages/Utilities/.gitignore new file mode 100644 index 0000000000..0023a53406 --- /dev/null +++ b/LocalPackages/Utilities/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LocalPackages/Utilities/Package.swift b/LocalPackages/Utilities/Package.swift new file mode 100644 index 0000000000..f66f812304 --- /dev/null +++ b/LocalPackages/Utilities/Package.swift @@ -0,0 +1,18 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Utilities", + products: [ + .library( + name: "Utilities", + targets: ["Utilities"]), + ], + targets: [ + .target( + name: "Utilities" + ) + ] +) diff --git a/DuckDuckGo/Common/Utilities/ArrayBuilder.swift b/LocalPackages/Utilities/Sources/Utilities/ArrayBuilder.swift similarity index 55% rename from DuckDuckGo/Common/Utilities/ArrayBuilder.swift rename to LocalPackages/Utilities/Sources/Utilities/ArrayBuilder.swift index e872e4d7bc..56e0e23075 100644 --- a/DuckDuckGo/Common/Utilities/ArrayBuilder.swift +++ b/LocalPackages/Utilities/Sources/Utilities/ArrayBuilder.swift @@ -19,73 +19,73 @@ import AppKit @resultBuilder -struct ArrayBuilder { +public struct ArrayBuilder { @inlinable - static func buildBlock() -> [Element] { + public static func buildBlock() -> [Element] { return [] } @inlinable - static func buildBlock(_ element: Element) -> [Element] { + public static func buildBlock(_ element: Element) -> [Element] { return [element] } @inlinable - static func buildBlock(_ elements: Element...) -> [Element] { + public static func buildBlock(_ elements: Element...) -> [Element] { return elements } @inlinable - static func buildBlock(_ components: [Element]...) -> [Element] { + public static func buildBlock(_ components: [Element]...) -> [Element] { return components.flatMap { $0 } } @inlinable - static func buildOptional(_ components: [Element]?) -> [Element] { + public static func buildOptional(_ components: [Element]?) -> [Element] { return components ?? [] } @inlinable - static func buildEither(first component: Element) -> [Element] { + public static func buildEither(first component: Element) -> [Element] { return [component] } @inlinable - static func buildEither(first component: [Element]) -> [Element] { + public static func buildEither(first component: [Element]) -> [Element] { component } @inlinable - static func buildEither(second component: [Element]) -> [Element] { + public static func buildEither(second component: [Element]) -> [Element] { component } - static func buildLimitedAvailability(_ component: [Element]) -> [Element] { + public static func buildLimitedAvailability(_ component: [Element]) -> [Element] { component } @inlinable - static func buildArray(_ components: [[Element]]) -> [Element] { + public static func buildArray(_ components: [[Element]]) -> [Element] { components.flatMap { $0 } } @inlinable - static func buildExpression(_ expression: [Element]) -> [Element] { + public static func buildExpression(_ expression: [Element]) -> [Element] { return expression } @inlinable - static func buildExpression(_ expression: Element) -> [Element] { + public static func buildExpression(_ expression: Element) -> [Element] { return [expression] } @inlinable - static func buildExpression(_ expression: Element?) -> [Element] { + public static func buildExpression(_ expression: Element?) -> [Element] { return expression.map { [$0] } ?? [] } - static func buildExpression(_ expression: Void) -> [Element] { + public static func buildExpression(_ expression: Void) -> [Element] { return [] } diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift index 8e901efdf3..11a89450c4 100644 --- a/LocalPackages/WebKitExtensions/Package.swift +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -33,12 +33,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.1"), + .package(path: "../AppKitExtensions") ], targets: [ .target( name: "WebKitExtensions", dependencies: [ .product(name: "UserScript", package: "BrowserServicesKit"), + "AppKitExtensions" ], swiftSettings: [ .define("DEBUG", .when(configuration: .debug)), diff --git a/DuckDuckGo/Common/Extensions/WKMenuItemIdentifier.swift b/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/WKMenuItemIdentifier.swift similarity index 89% rename from DuckDuckGo/Common/Extensions/WKMenuItemIdentifier.swift rename to LocalPackages/WebKitExtensions/Sources/WebKitExtensions/WKMenuItemIdentifier.swift index eb4f388879..154fa9316b 100644 --- a/DuckDuckGo/Common/Extensions/WKMenuItemIdentifier.swift +++ b/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/WKMenuItemIdentifier.swift @@ -16,9 +16,9 @@ // limitations under the License. // -import AppKit +import AppKitExtensions -enum WKMenuItemIdentifier: String, CaseIterable { +public enum WKMenuItemIdentifier: String, CaseIterable { case copy = "WKMenuItemIdentifierCopy" case copyImage = "WKMenuItemIdentifierCopyImage" case copyLink = "WKMenuItemIdentifierCopyLink" @@ -58,7 +58,13 @@ enum WKMenuItemIdentifier: String, CaseIterable { case checkSpellingWhileTyping = "WKMenuItemIdentifierCheckSpellingWhileTyping" case checkGrammarWithSpelling = "WKMenuItemIdentifierCheckGrammarWithSpelling" - init?(_ identifier: NSUserInterfaceItemIdentifier) { + public init?(_ identifier: NSUserInterfaceItemIdentifier) { self.init(rawValue: identifier.rawValue) } } + +public extension NSMenu { + func item(with identifier: WKMenuItemIdentifier) -> NSMenuItem? { + return indexOfItem(withIdentifier: identifier.rawValue).map { self.items[$0] } + } +} diff --git a/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/export.swift b/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/export.swift new file mode 100644 index 0000000000..92f4846bd1 --- /dev/null +++ b/LocalPackages/WebKitExtensions/Sources/WebKitExtensions/export.swift @@ -0,0 +1,19 @@ +// +// export.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. +// + +@_exported import WebKit From b1cac9b8148fdcd64997824faea0adc38b17c3b3 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 11:58:58 +0100 Subject: [PATCH 18/62] Re-add migrating favorites view expanded state --- .../NewTabPageActionsManagerExtension.swift | 7 ++-- .../Favorites/NewTabPageFavoritesModel.swift | 32 +++++++++++++------ .../NewTabPagePrivacyStatsModel.swift | 5 +-- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index e52873c650..63549e46dd 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -31,12 +31,15 @@ extension NewTabPageActionsManager { let privacyStatsModel = NewTabPagePrivacyStatsModel( privacyStats: privacyStats, trackerDataProvider: PrivacyStatsTrackerDataProvider(contentBlocking: ContentBlocking.shared), - keyValueStore: UserDefaults.standard, getLegacyIsViewExpandedSetting: UserDefaultsWrapper(key: .homePageShowRecentlyVisited, defaultValue: false).wrappedValue ) let favoritesPublisher = LocalBookmarkManager.shared.listPublisher.map({ $0?.favoriteBookmarks ?? [] }).eraseToAnyPublisher() - let favoritesModel = NewTabPageFavoritesModel(actionsHandler: DefaultFavoritesActionsHandler(), favoritesPublisher: favoritesPublisher) + let favoritesModel = NewTabPageFavoritesModel( + actionsHandler: DefaultFavoritesActionsHandler(), + favoritesPublisher: favoritesPublisher, + getLegacyIsViewExpandedSetting: UserDefaultsWrapper(key: .homePageShowAllFavorites, defaultValue: false).wrappedValue + ) self.init(scriptClients: [ NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences), diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift index 04056b5768..dce3f7bde4 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift @@ -32,9 +32,9 @@ public final class UserDefaultsNewTabPageFavoritesSettingsPersistor: NewTabPageF private let keyValueStore: KeyValueStoring - public init(_ keyValueStore: KeyValueStoring = UserDefaults.standard) { + public init(_ keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacySetting: @autoclosure () -> Bool?) { self.keyValueStore = keyValueStore - migrateFromNativeHomePageSettings() + migrateFromLegacyHomePageSettings(using: getLegacySetting) } public var isViewExpanded: Bool { @@ -42,12 +42,11 @@ public final class UserDefaultsNewTabPageFavoritesSettingsPersistor: NewTabPageF set { keyValueStore.set(newValue, forKey: Keys.isViewExpanded) } } - private func migrateFromNativeHomePageSettings() { -// guard keyValueStore.object(forKey: Keys.isViewExpanded) == nil else { -// return -// } -// let legacyKey = UserDefaultsWrapper.Key.homePageShowAllFavorites.rawValue -// isViewExpanded = keyValueStore.object(forKey: legacyKey) as? Bool ?? false + private func migrateFromLegacyHomePageSettings(using getLegacySetting: () -> Bool?) { + guard keyValueStore.object(forKey: Keys.isViewExpanded) == nil, let legacySetting = getLegacySetting() else { + return + } + isViewExpanded = legacySetting } } @@ -64,11 +63,26 @@ public final class NewTabPageFavoritesModel: NSObje private let settingsPersistor: NewTabPageFavoritesSettingsPersistor private var cancellables: Set = [] + public convenience init( + actionsHandler: ActionHandler, + favoritesPublisher: AnyPublisher<[FavoriteType], Never>, + contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), + keyValueStore: KeyValueStoring = UserDefaults.standard, + getLegacyIsViewExpandedSetting: @autoclosure () -> Bool? + ) { + self.init( + actionsHandler: actionsHandler, + favoritesPublisher: favoritesPublisher, + contextMenuPresenter: contextMenuPresenter, + settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor(keyValueStore, getLegacySetting: getLegacyIsViewExpandedSetting()) + ) + } + public init( actionsHandler: ActionHandler, favoritesPublisher: AnyPublisher<[FavoriteType], Never>, contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), - settingsPersistor: NewTabPageFavoritesSettingsPersistor = UserDefaultsNewTabPageFavoritesSettingsPersistor() + settingsPersistor: NewTabPageFavoritesSettingsPersistor ) { self.actionsHandler = actionsHandler self.contextMenuPresenter = contextMenuPresenter diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift index 99b3d4d5d8..e5ba46311b 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift @@ -17,6 +17,7 @@ // import Combine +import Foundation import os.log import Persistence import PrivacyStats @@ -32,7 +33,7 @@ public final class UserDefaultsNewTabPagePrivacyStatsSettingsPersistor: NewTabPa private let keyValueStore: KeyValueStoring - public init(_ keyValueStore: KeyValueStoring, getLegacySetting: @autoclosure () -> Bool?) { + public init(_ keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacySetting: @autoclosure () -> Bool?) { self.keyValueStore = keyValueStore migrateFromLegacyHomePageSettings(using: getLegacySetting) } @@ -71,7 +72,7 @@ public final class NewTabPagePrivacyStatsModel { public convenience init( privacyStats: PrivacyStatsCollecting, trackerDataProvider: PrivacyStatsTrackerDataProviding, - keyValueStore: KeyValueStoring, + keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacyIsViewExpandedSetting: @autoclosure () -> Bool? ) { self.init( From 7bb29beb1b776c670b08852e96da3e3bb5551356 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 12:46:53 +0100 Subject: [PATCH 19/62] Move favorites tests --- DuckDuckGo.xcodeproj/project.pbxproj | 38 ++--- .../ActiveRemoteMessageModel+NewTabPage.swift | 27 ++++ LocalPackages/NewTabPage/Package.swift | 3 + .../Favorites/NewTabPageFavoritesModel.swift | 2 +- .../NewTabPage/RMF}/NewTabPageRMFClient.swift | 82 +++++----- ...ingNewTabPageFavoritesActionsHandler.swift | 80 ++++++++++ .../Mocks/MockNewTabPageFavorite.swift | 29 ++++ .../NewTabPageFavoritesClientTests.swift | 151 +++++------------- .../NewTabPageFavoritesModelTests.swift | 32 ++-- .../Model/LocalBookmarkManagerTests.swift | 1 + .../Common/Extensions/URLExtensionTests.swift | 21 --- 11 files changed, 252 insertions(+), 214 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift rename {DuckDuckGo/NewTabPage => LocalPackages/NewTabPage/Sources/NewTabPage/RMF}/NewTabPageRMFClient.swift (82%) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFavoritesActionsHandler.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageFavorite.swift rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageFavoritesClientTests.swift (56%) rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageFavoritesModelTests.swift (66%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 672aeb29d3..f2c768a48e 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1106,8 +1106,6 @@ 37219B3E2CC27DB700C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */; }; 372217802B3337FE00B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 3722177F2B3337FE00B8E9C2 /* TestUtils */; }; 372217822B33380700B8E9C2 /* TestUtils in Frameworks */ = {isa = PBXBuildFile; productRef = 372217812B33380700B8E9C2 /* TestUtils */; }; - 3723A94F2CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; - 3723A9502CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */; }; 37269EFB2B332F9E005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFA2B332F9E005E8E46 /* Common */; }; 37269EFD2B332FAC005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFC2B332FAC005E8E46 /* Common */; }; 37269EFF2B332FBB005E8E46 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 37269EFE2B332FBB005E8E46 /* Common */; }; @@ -1143,6 +1141,10 @@ 374EF08429B7575B003D2E87 /* RecentlyClosedCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */; }; 374EFDEB2D01A1D800B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEA2D01A1D800B30939 /* Utilities */; }; 374EFDED2D01A1DE00B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEC2D01A1DE00B30939 /* Utilities */; }; + 374EFDEF2D01C70300B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDEE2D01C70300B30939 /* Utilities */; }; + 374EFDF12D01C70A00B30939 /* Utilities in Frameworks */ = {isa = PBXBuildFile; productRef = 374EFDF02D01C70A00B30939 /* Utilities */; }; + 374EFDF32D01C99E00B30939 /* ActiveRemoteMessageModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */; }; + 374EFDF42D01C99E00B30939 /* ActiveRemoteMessageModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */; }; 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534C9D28104D9B002621E7 /* TabLazyLoaderTests.swift */; }; 37534CA028113101002621E7 /* LazyLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534C9F28113101002621E7 /* LazyLoadable.swift */; }; 37534CA3281132CB002621E7 /* TabLazyLoaderDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA2281132CB002621E7 /* TabLazyLoaderDataSource.swift */; }; @@ -1200,8 +1202,6 @@ 3783F92329432E1800BCA897 /* WebViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3783F92229432E1800BCA897 /* WebViewTests.swift */; }; 37878E552CA3330300CC9EB5 /* HomePageAddressBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37878E542CA332F800CC9EB5 /* HomePageAddressBarModel.swift */; }; 37878E562CA3330300CC9EB5 /* HomePageAddressBarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37878E542CA332F800CC9EB5 /* HomePageAddressBarModel.swift */; }; - 378D62572CEF80200056BBD8 /* NewTabPageFavoritesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */; }; - 378D62582CEF80200056BBD8 /* NewTabPageFavoritesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */; }; 378F44E429B4BDE900899924 /* SwiftUIExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 378F44E329B4BDE900899924 /* SwiftUIExtensions */; }; 378F44E629B4BDEE00899924 /* SwiftUIExtensions in Frameworks */ = {isa = PBXBuildFile; productRef = 378F44E529B4BDEE00899924 /* SwiftUIExtensions */; }; 378F44EB29B4C73E00899924 /* ViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 378F44EA29B4C73E00899924 /* ViewExtension.swift */; }; @@ -1297,8 +1297,6 @@ 37F19A6528E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6428E1B3FB00740DC6 /* PreferencesDuckPlayerView.swift */; }; 37F19A6728E1B43200740DC6 /* DuckPlayerPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */; }; 37F19A6A28E2F2D000740DC6 /* DuckPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */; }; - 37F1E32F2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */; }; - 37F1E3302CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */; }; 37F44A5F298C17830025E7FE /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 37F44A5E298C17830025E7FE /* Navigation */; }; 37F8ABD32CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; 37F8ABD42CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; @@ -3638,7 +3636,6 @@ 37219B362CBFBC8200C9D7A8 /* NewTabSearchBoxExperimentPixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabSearchBoxExperimentPixel.swift; sourceTree = ""; }; 37219B392CBFD4F300C9D7A8 /* NewTabPageSearchBoxExperiment+Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NewTabPageSearchBoxExperiment+Logger.swift"; sourceTree = ""; }; 37219B3C2CC27DB300C9D7A8 /* NewTabPageSearchBoxExperimentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageSearchBoxExperimentTests.swift; sourceTree = ""; }; - 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClient.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 = ""; }; 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ContinueSetUpModel+NewTabPage.swift"; sourceTree = ""; }; @@ -3655,6 +3652,7 @@ 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapter.swift; sourceTree = ""; }; 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModelTests+WithoutPinnedTabsManager.swift"; sourceTree = ""; }; 374EF08229B751FC003D2E87 /* RecentlyClosedCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyClosedCoordinatorTests.swift; sourceTree = ""; }; + 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActiveRemoteMessageModel+NewTabPage.swift"; sourceTree = ""; }; 374F18B32D006F940032EA4E /* NewTabPage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewTabPage; sourceTree = ""; }; 37534C9D28104D9B002621E7 /* TabLazyLoaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabLazyLoaderTests.swift; sourceTree = ""; }; 37534C9F28113101002621E7 /* LazyLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyLoadable.swift; sourceTree = ""; }; @@ -3704,7 +3702,6 @@ 378B588D295CF447002C0CC0 /* UnitTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = UnitTests.xcconfig; sourceTree = ""; }; 378B58C8295CF9A7002C0CC0 /* IntegrationTests.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = IntegrationTests.xcconfig; sourceTree = ""; }; 378B58CD295ECA75002C0CC0 /* DuckDuckGo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DuckDuckGo.xcconfig; sourceTree = ""; }; - 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesModelTests.swift; sourceTree = ""; }; 378E2799296F6FDE00FCADA2 /* ManualAppStoreRelease.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ManualAppStoreRelease.xcconfig; sourceTree = ""; }; 378E279D2970217400FCADA2 /* BuildToolPlugins */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = BuildToolPlugins; sourceTree = ""; }; 378F44E229B4B7B600899924 /* SwiftUIExtensions */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = SwiftUIExtensions; sourceTree = ""; }; @@ -3774,7 +3771,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 = ""; }; - 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFavoritesClientTests.swift; sourceTree = ""; }; 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverridesMenu.swift; sourceTree = ""; }; 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyStats.swift; sourceTree = ""; }; 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncErrorHandler.swift; sourceTree = ""; }; @@ -5057,6 +5053,7 @@ 84BBC7FF2CFA0D2F00BAE57A /* TestUtils in Frameworks */, B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */, 9DC5FACD2C6B8E620011F068 /* AppKitExtensions in Frameworks */, + 374EFDF12D01C70A00B30939 /* Utilities in Frameworks */, F1DA51A52BF6114200CF29FA /* SubscriptionTestingUtilities in Frameworks */, 3706FE89293F661700E42796 /* OHHTTPStubsSwift in Frameworks */, ); @@ -5326,6 +5323,7 @@ 84BBC8012CFA0D3800BAE57A /* TestUtils in Frameworks */, B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */, 9DC5FACB2C6B8E050011F068 /* AppKitExtensions in Frameworks */, + 374EFDEF2D01C70300B30939 /* Utilities in Frameworks */, F1DA51A92BF6114C00CF29FA /* SubscriptionTestingUtilities in Frameworks */, B6DA44192616C13800DD1EC2 /* OHHTTPStubsSwift in Frameworks */, ); @@ -5819,11 +5817,11 @@ 376788092CECCAD900F59D83 /* NewTabPage */ = { isa = PBXGroup; children = ( + 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */, 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */, 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */, 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */, 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */, - 3723A94E2CE73FDD00A0C59A /* NewTabPageRMFClient.swift */, 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */, ); path = NewTabPage; @@ -5834,8 +5832,6 @@ children = ( 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */, 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */, - 37F1E32E2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift */, - 378D62562CEF801A0056BBD8 /* NewTabPageFavoritesModelTests.swift */, ); path = NewTabPage; sourceTree = ""; @@ -9979,6 +9975,7 @@ F1DA51A42BF6114200CF29FA /* SubscriptionTestingUtilities */, 9DC5FACC2C6B8E620011F068 /* AppKitExtensions */, 84BBC7FE2CFA0D2F00BAE57A /* TestUtils */, + 374EFDF02D01C70A00B30939 /* Utilities */, ); productName = DuckDuckGoTests; productReference = 3706FE99293F661700E42796 /* Unit Tests App Store.xctest */; @@ -10462,6 +10459,7 @@ F1DA51A82BF6114C00CF29FA /* SubscriptionTestingUtilities */, 9DC5FACA2C6B8E050011F068 /* AppKitExtensions */, 84BBC8002CFA0D3800BAE57A /* TestUtils */, + 374EFDEE2D01C70300B30939 /* Utilities */, ); productName = DuckDuckGoTests; productReference = AA585D90248FD31400E9A3E2 /* Unit Tests.xctest */; @@ -11574,7 +11572,6 @@ 37E2608D2C8A1F6D006EE07F /* UserColorProviding.swift in Sources */, BDBA85A02C5D25B700BC54F5 /* VPNMetadataCollector.swift in Sources */, 3706FB60293F65D500E42796 /* PasswordManagementIdentityItemView.swift in Sources */, - 3723A94F2CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */, 3706FB61293F65D500E42796 /* ProgressExtension.swift in Sources */, 3706FB62293F65D500E42796 /* CSVParser.swift in Sources */, 31C26A0E2CBE9DFE00FFF462 /* AIChatPreferences.swift in Sources */, @@ -11882,6 +11879,7 @@ 37FC2A192CF903080048E226 /* MockPrivacyStats.swift in Sources */, B60C6F7829B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, 9F56CFB22B843F6C00BB7F11 /* BookmarksDialogViewFactory.swift in Sources */, + 374EFDF42D01C99E00B30939 /* ActiveRemoteMessageModel+NewTabPage.swift in Sources */, 3707C720294B5D2900682A9F /* WKWebsiteDataStoreExtension.swift in Sources */, 5677A9382C9812E800DA7B0A /* TrackerMessageProvider.swift in Sources */, 3706FC03293F65D500E42796 /* TabPreviewViewController.swift in Sources */, @@ -12325,13 +12323,11 @@ 562532A12BC069190034D316 /* ZoomPopoverViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, - 37F1E3302CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift in Sources */, 567A23CF2C80CF4B0010F66C /* ErrorPageTabExtensionTest.swift in Sources */, 37D046A22C7DA9A200AEAA50 /* UserBackgroundImagesManagerTests.swift in Sources */, 37DB56F02C3B31CD0093D4DC /* MockRemoteMessagingAvailabilityProvider.swift in Sources */, 567A23E22C89B1EE0010F66C /* BrowserTabViewControllerOnboardingTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, - 378D62572CEF80200056BBD8 /* NewTabPageFavoritesModelTests.swift in Sources */, 56A0540E2C1C375E007D8FAB /* MockWindow.swift in Sources */, 3706FE2A293F661700E42796 /* SafariVersionReaderTests.swift in Sources */, 31031EB82CC94C6E00684340 /* AIChatRemoteSettingsTests.swift in Sources */, @@ -13196,7 +13192,6 @@ 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */, 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 85589E9127BFB9810038AD11 /* HomePageRecentlyVisitedModel.swift in Sources */, - 3723A9502CE7400D00A0C59A /* NewTabPageRMFClient.swift in Sources */, 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */, B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, 37D23785287F4E6500BCE03B /* PinnedTabsHostingView.swift in Sources */, @@ -13522,6 +13517,7 @@ 3148727C2CC689C200EEF89B /* AIChatToolBarPopUpOnboardingViewModel.swift in Sources */, AABEE6A924AB4B910043105B /* SuggestionTableCellView.swift in Sources */, AA6820F125503DA9005ED0D5 /* FireViewModel.swift in Sources */, + 374EFDF32D01C99E00B30939 /* ActiveRemoteMessageModel+NewTabPage.swift in Sources */, 372BC2A12A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift in Sources */, 4BE65479271FCD41008D1D63 /* EditableTextView.swift in Sources */, AA9FF95D24A1FA1C0039E328 /* TabCollection.swift in Sources */, @@ -13881,7 +13877,6 @@ 4BF6961D28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift in Sources */, 9FA5A0A92BC900FC00153786 /* BookmarkAllTabsDialogViewModelTests.swift in Sources */, B6619F062B17138D00CD9186 /* DataImportSourceViewModelTests.swift in Sources */, - 378D62582CEF80200056BBD8 /* NewTabPageFavoritesModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, EE66F10C2C3431030071856E /* WebsiteAccount_isDuplicateTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, @@ -13947,7 +13942,6 @@ 1D7693FF2BE3A1AA0016A22B /* DockCustomizerMock.swift in Sources */, 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, 376731872C7EF9C200EB097B /* ColorSchemeLosslessStringConvertibleExtensionTests.swift in Sources */, - 37F1E32F2CEF4A2C00130142 /* NewTabPageFavoritesClientTests.swift in Sources */, 9FA5A0B02BC9039200153786 /* BookmarkFolderStoreMock.swift in Sources */, F1F861152C1B25D4005DB446 /* SubscriptionUIHandlerMock.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, @@ -15573,6 +15567,14 @@ isa = XCSwiftPackageProductDependency; productName = Utilities; }; + 374EFDEE2D01C70300B30939 /* Utilities */ = { + isa = XCSwiftPackageProductDependency; + productName = Utilities; + }; + 374EFDF02D01C70A00B30939 /* Utilities */ = { + isa = XCSwiftPackageProductDependency; + productName = Utilities; + }; 378F44E329B4BDE900899924 /* SwiftUIExtensions */ = { isa = XCSwiftPackageProductDependency; productName = SwiftUIExtensions; diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift new file mode 100644 index 0000000000..b4f4d8684c --- /dev/null +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -0,0 +1,27 @@ +// +// ActiveRemoteMessageModel+NewTabPage.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 NewTabPage +import RemoteMessaging + +extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { + var remoteMessagePublisher: AnyPublisher { + $remoteMessage.dropFirst().eraseToAnyPublisher() + } +} diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index 1727f99c10..376b5ba786 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -34,6 +34,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.1"), .package(path: "../WebKitExtensions"), + .package(path: "../Utilities"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -43,6 +44,7 @@ let package = Package( dependencies: [ .product(name: "BrowserServicesKit", package: "BrowserServicesKit"), .product(name: "PrivacyStats", package: "BrowserServicesKit"), + .product(name: "RemoteMessaging", package: "BrowserServicesKit"), .product(name: "TestUtils", package: "BrowserServicesKit"), .product(name: "WebKitExtensions", package: "WebKitExtensions"), ], @@ -54,6 +56,7 @@ let package = Package( name: "NewTabPageTests", dependencies: [ "NewTabPage", + "Utilities", ] ), ] diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift index dce3f7bde4..bed102fb47 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift @@ -112,7 +112,7 @@ public final class NewTabPageFavoritesModel: NSObje @MainActor public func openFavorite(withURL url: String) { - guard let url = URL(string: url) else { return } + guard let url = URL(string: url), url.isValid else { return } actionsHandler.open(url, target: .current) } diff --git a/DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift similarity index 82% rename from DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift index 38315b5b02..ace1515a89 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageRMFClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift @@ -18,32 +18,26 @@ import Combine import Common -import NewTabPage import RemoteMessaging import UserScript +import WebKit -protocol NewTabPageActiveRemoteMessageProviding { +public protocol NewTabPageActiveRemoteMessageProviding { var remoteMessage: RemoteMessageModel? { get set } var remoteMessagePublisher: AnyPublisher { get } func dismissRemoteMessage(with action: RemoteMessageViewModel.ButtonAction?) async } -extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { - var remoteMessagePublisher: AnyPublisher { - $remoteMessage.dropFirst().eraseToAnyPublisher() - } -} - -final class NewTabPageRMFClient: NewTabPageScriptClient { +public final class NewTabPageRMFClient: NewTabPageScriptClient { - let remoteMessageProvider: NewTabPageActiveRemoteMessageProviding - weak var userScriptsSource: NewTabPageUserScriptsSource? + public let remoteMessageProvider: NewTabPageActiveRemoteMessageProviding + public weak var userScriptsSource: NewTabPageUserScriptsSource? private let openURLHandler: (URL) -> Void private var cancellables = Set() - init(remoteMessageProvider: NewTabPageActiveRemoteMessageProviding, openURLHandler: @escaping (URL) -> Void) { + public init(remoteMessageProvider: NewTabPageActiveRemoteMessageProviding, openURLHandler: @escaping (URL) -> Void) { self.remoteMessageProvider = remoteMessageProvider self.openURLHandler = openURLHandler @@ -54,7 +48,7 @@ final class NewTabPageRMFClient: NewTabPageScriptClient { .store(in: &cancellables) } - enum MessageName: String, CaseIterable { + public enum MessageName: String, CaseIterable { case rmfGetData = "rmf_getData" case rmfOnDataUpdate = "rmf_onDataUpdate" case rmfDismiss = "rmf_dismiss" @@ -62,7 +56,7 @@ final class NewTabPageRMFClient: NewTabPageScriptClient { case rmfSecondaryAction = "rmf_secondaryAction" } - func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + public func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { userScript.registerMessageHandlers([ MessageName.rmfGetData.rawValue: { [weak self] in try await self?.getData(params: $0, original: $1) }, MessageName.rmfDismiss.rawValue: { [weak self] in try await self?.dismiss(params: $0, original: $1) }, @@ -153,24 +147,24 @@ final class NewTabPageRMFClient: NewTabPageScriptClient { } } -extension NewTabPageUserScript { +public extension NewTabPageUserScript { struct RemoteMessageParams: Codable { - let id: String + public let id: String } struct RMFData: Encodable { - let content: RMFMessage? + public let content: RMFMessage? } enum RMFMessage: Encodable, Equatable { case small(SmallMessage), medium(MediumMessage), bigSingleAction(BigSingleActionMessage), bigTwoAction(BigTwoActionMessage) - func encode(to encoder: any Encoder) throws { + public func encode(to encoder: any Encoder) throws { try message.encode(to: encoder) } - var message: Encodable { + public var message: Encodable { switch self { case .small(let message): return message @@ -183,7 +177,7 @@ extension NewTabPageUserScript { } } - init?(_ remoteMessageModel: RemoteMessageModel) { + public init?(_ remoteMessageModel: RemoteMessageModel) { guard let modelType = remoteMessageModel.content, modelType.isSupported else { return nil } @@ -208,41 +202,41 @@ extension NewTabPageUserScript { } struct SmallMessage: Encodable, Equatable { - let messageType = "small" + public let messageType = "small" - let id: String - let titleText: String - let descriptionText: String + public let id: String + public let titleText: String + public let descriptionText: String } struct MediumMessage: Encodable, Equatable { - let messageType = "medium" + public let messageType = "medium" - let id: String - let titleText: String - let descriptionText: String - let icon: RMFIcon + public let id: String + public let titleText: String + public let descriptionText: String + public let icon: RMFIcon } struct BigSingleActionMessage: Encodable, Equatable { - let messageType = "big_single_action" + public let messageType = "big_single_action" - let id: String - let titleText: String - let descriptionText: String - let icon: RMFIcon - let primaryActionText: String + public let id: String + public let titleText: String + public let descriptionText: String + public let icon: RMFIcon + public let primaryActionText: String } struct BigTwoActionMessage: Encodable, Equatable { - let messageType = "big_two_action" - - let id: String - let titleText: String - let descriptionText: String - let icon: RMFIcon - let primaryActionText: String - let secondaryActionText: String + public let messageType = "big_two_action" + + public let id: String + public let titleText: String + public let descriptionText: String + public let icon: RMFIcon + public let primaryActionText: String + public let secondaryActionText: String } enum RMFIcon: String, Encodable { @@ -252,7 +246,7 @@ extension NewTabPageUserScript { case appUpdate = "AppUpdate" case privacyPro = "PrivacyPro" - init(_ placeholder: RemotePlaceholder) { + public init(_ placeholder: RemotePlaceholder) { switch placeholder { case .announce: self = .announce diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFavoritesActionsHandler.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFavoritesActionsHandler.swift new file mode 100644 index 0000000000..f0fde984b4 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFavoritesActionsHandler.swift @@ -0,0 +1,80 @@ +// +// CapturingNewTabPageFavoritesActionsHandler.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 NewTabPage + +final class CapturingNewTabPageFavoritesActionsHandler: FavoritesActionsHandling { + typealias FavoriteType = MockNewTabPageFavorite + + struct OpenCall: Equatable { + let url: URL + let target: FavoriteOpenTarget + + init(_ url: URL, _ target: FavoriteOpenTarget) { + self.url = url + self.target = target + } + } + + struct MoveCall: Equatable { + let id: String + let toIndex: Int + + init(_ id: String, _ toIndex: Int) { + self.id = id + self.toIndex = toIndex + } + } + + var openCalls: [OpenCall] = [] + var addNewFavoriteCallCount: Int = 0 + var editCalls: [MockNewTabPageFavorite] = [] + var onFaviconMissingCallCount: Int = 0 + var removeFavoriteCalls: [MockNewTabPageFavorite] = [] + var deleteBookmarkCalls: [MockNewTabPageFavorite] = [] + var moveCalls: [MoveCall] = [] + + func open(_ url: URL, target: FavoriteOpenTarget) { + openCalls.append(.init(url, target)) + } + + func addNewFavorite() { + addNewFavoriteCallCount += 1 + } + + func edit(_ favorite: MockNewTabPageFavorite) { + editCalls.append(favorite) + } + + func onFaviconMissing() { + onFaviconMissingCallCount += 1 + } + + func removeFavorite(_ favorite: MockNewTabPageFavorite) { + removeFavoriteCalls.append(favorite) + } + + func deleteBookmark(for favorite: MockNewTabPageFavorite) { + deleteBookmarkCalls.append(favorite) + } + + func move(_ bookmarkID: String, toIndex: Int) { + moveCalls.append(.init(bookmarkID, toIndex)) + } +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageFavorite.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageFavorite.swift new file mode 100644 index 0000000000..d9cca07405 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/MockNewTabPageFavorite.swift @@ -0,0 +1,29 @@ +// +// MockNewTabPageFavorite.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 NewTabPage + +struct MockNewTabPageFavorite: NewTabPageFavorite, Equatable { + var id: String + var title: String + var url: String + var urlObject: URL? { + URL(string: url) + } +} diff --git a/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift similarity index 56% rename from UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index eb20beb26a..4c466c1b6a 100644 --- a/UnitTests/NewTabPage/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -21,79 +21,15 @@ import NewTabPage import RemoteMessaging import TestUtils import XCTest -@testable import DuckDuckGo_Privacy_Browser - -final class CapturingNewTabPageContextMenuPresenter: NewTabPageContextMenuPresenting { - func showContextMenu(_ menu: NSMenu) { - showContextMenuCalls.append(menu) - } - - var showContextMenuCalls: [NSMenu] = [] -} - -final class CapturingNewTabPageFavoritesActionsHandler: FavoritesActionsHandling { - struct OpenCall: Equatable { - let url: URL - let target: NewTabPageFavoritesModel.OpenTarget - - init(_ url: URL, _ target: NewTabPageFavoritesModel.OpenTarget) { - self.url = url - self.target = target - } - } - - struct MoveCall: Equatable { - let id: String - let toIndex: Int - - init(_ id: String, _ toIndex: Int) { - self.id = id - self.toIndex = toIndex - } - } - - var openCalls: [OpenCall] = [] - var addNewFavoriteCallCount: Int = 0 - var editCalls: [Bookmark] = [] - var onFaviconMissingCallCount: Int = 0 - var removeFavoriteCalls: [Bookmark] = [] - var deleteBookmarkCalls: [Bookmark] = [] - var moveCalls: [MoveCall] = [] - - func open(_ url: URL, target: NewTabPageFavoritesModel.OpenTarget) { - openCalls.append(.init(url, target)) - } - - func addNewFavorite() { - addNewFavoriteCallCount += 1 - } - - func edit(_ bookmark: Bookmark) { - editCalls.append(bookmark) - } - - func onFaviconMissing() { - onFaviconMissingCallCount += 1 - } - - func removeFavorite(_ bookmark: Bookmark) { - removeFavoriteCalls.append(bookmark) - } - - func deleteBookmark(_ bookmark: Bookmark) { - deleteBookmarkCalls.append(bookmark) - } - - func move(_ bookmarkID: String, toIndex: Int) { - moveCalls.append(.init(bookmarkID, toIndex)) - } -} +@testable import NewTabPage final class NewTabPageFavoritesClientTests: XCTestCase { - var client: NewTabPageFavoritesClient! + typealias NewTabPageFavoritesClientUnderTest = NewTabPageFavoritesClient + + var client: NewTabPageFavoritesClientUnderTest! var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! var actionsHandler: CapturingNewTabPageFavoritesActionsHandler! - var favoritesModel: NewTabPageFavoritesModel! + var favoritesModel: NewTabPageFavoritesModel! var userScript: NewTabPageUserScript! @MainActor @@ -101,10 +37,11 @@ final class NewTabPageFavoritesClientTests: XCTestCase { try super.setUpWithError() contextMenuPresenter = CapturingNewTabPageContextMenuPresenter() actionsHandler = CapturingNewTabPageFavoritesActionsHandler() - favoritesModel = NewTabPageFavoritesModel( + favoritesModel = NewTabPageFavoritesModel.init( actionsHandler: actionsHandler, + favoritesPublisher: Empty().eraseToAnyPublisher(), contextMenuPresenter: contextMenuPresenter, - settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor(MockKeyValueStore()) + settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor(MockKeyValueStore(), getLegacySetting: nil) ) client = NewTabPageFavoritesClient(favoritesModel: favoritesModel) @@ -156,13 +93,13 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testThatGetDataReturnsFavoritesFromTheModel() async throws { favoritesModel.favorites = [ - Bookmark(id: "1", url: "https://a.com", title: "A", isFavorite: true), - Bookmark(id: "10", url: "https://b.com", title: "B", isFavorite: true), - Bookmark(id: "5", url: "https://c.com", title: "C", isFavorite: true), - Bookmark(id: "2", url: "https://d.com", title: "D", isFavorite: true), - Bookmark(id: "3", url: "https://e.com", title: "E", isFavorite: true) + MockNewTabPageFavorite(id: "1", title: "A", url: "https://a.com"), + MockNewTabPageFavorite(id: "10", title: "B", url: "https://b.com"), + MockNewTabPageFavorite(id: "5", title: "C", url: "https://c.com"), + MockNewTabPageFavorite(id: "2", title: "D", url: "https://d.com"), + MockNewTabPageFavorite(id: "3", title: "E", url: "https://e.com") ] - let data: NewTabPageFavoritesClient.FavoritesData = try await handleMessage(named: .getData) + let data: NewTabPageFavoritesClientUnderTest.FavoritesData = try await handleMessage(named: .getData) XCTAssertEqual(data.favorites, [ .init(id: "1", title: "A", url: "https://a.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//a.com")), .init(id: "10", title: "B", url: "https://b.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//b.com")), @@ -174,20 +111,20 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testWhenFavoritesAreEmptyThenGetDataReturnsNoFavorites() async throws { favoritesModel.favorites = [] - let data: NewTabPageFavoritesClient.FavoritesData = try await handleMessage(named: .getData) + let data: NewTabPageFavoritesClientUnderTest.FavoritesData = try await handleMessage(named: .getData) XCTAssertEqual(data.favorites, []) } // MARK: - move func testThatMoveActionIsForwardedToTheModel() async throws { - let action = NewTabPageFavoritesClient.FavoritesMoveAction(id: "abcd", fromIndex: 10, targetIndex: 4) + let action = NewTabPageFavoritesClientUnderTest.FavoritesMoveAction(id: "abcd", fromIndex: 10, targetIndex: 4) try await handleMessageExpectingNilResponse(named: .move, parameters: action) XCTAssertEqual(actionsHandler.moveCalls, [.init("abcd", 4)]) } func testThatWhenFavoriteIsMovedToHigherIndexThenModelIncrementsIndex() async throws { - let action = NewTabPageFavoritesClient.FavoritesMoveAction(id: "abcd", fromIndex: 1, targetIndex: 4) + let action = NewTabPageFavoritesClientUnderTest.FavoritesMoveAction(id: "abcd", fromIndex: 1, targetIndex: 4) try await handleMessageExpectingNilResponse(named: .move, parameters: action) XCTAssertEqual(actionsHandler.moveCalls, [.init("abcd", 5)]) } @@ -195,13 +132,13 @@ final class NewTabPageFavoritesClientTests: XCTestCase { // MARK: - open func testThatOpenActionIsForwardedToTheModel() async throws { - let action = NewTabPageFavoritesClient.FavoritesOpenAction(id: "abcd", url: "https://example.com") + let action = NewTabPageFavoritesClientUnderTest.FavoritesOpenAction(id: "abcd", url: "https://example.com") try await handleMessageExpectingNilResponse(named: .open, parameters: action) - XCTAssertEqual(actionsHandler.openCalls, [.init("https://example.com".url!, .current)]) + XCTAssertEqual(actionsHandler.openCalls, [.init(URL(string: "https://example.com")!, .current)]) } func testWhenURLIsInvalidThenOpenActionIsNotForwardedToTheModel() async throws { - let action = NewTabPageFavoritesClient.FavoritesOpenAction(id: "abcd", url: "abcd") + let action = NewTabPageFavoritesClientUnderTest.FavoritesOpenAction(id: "abcd", url: "abcd") try await handleMessageExpectingNilResponse(named: .open, parameters: action) XCTAssertEqual(actionsHandler.openCalls, []) } @@ -209,54 +146,40 @@ final class NewTabPageFavoritesClientTests: XCTestCase { // MARK: - openContextMenu func testThatOpenContextMenuActionForExistingFavoriteIsForwardedToTheModel() async throws { - favoritesModel.favorites = [.init(id: "abcd", url: "https://example.com", title: "A", isFavorite: true)] - let action = NewTabPageFavoritesClient.FavoritesContextMenuAction(id: "abcd") + favoritesModel.favorites = [.init(id: "abcd", title: "A", url: "https://example.com")] + let action = NewTabPageFavoritesClientUnderTest.FavoritesContextMenuAction(id: "abcd") try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 1) } func testThatOpenContextMenuActionForNotExistingFavoriteIsNotForwardedToTheModel() async throws { favoritesModel.favorites = [] - let action = NewTabPageFavoritesClient.FavoritesContextMenuAction(id: "abcd") + let action = NewTabPageFavoritesClientUnderTest.FavoritesContextMenuAction(id: "abcd") try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 0) } - @MainActor - func testThatContextMenuActionsAreForwardedToTheHandler() async throws { - favoritesModel.favorites = [.init(id: "abcd", url: "https://example.com", title: "A", isFavorite: true)] - let action = NewTabPageFavoritesClient.FavoritesContextMenuAction(id: "abcd") - try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) - XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 1) - - let menu = try XCTUnwrap(contextMenuPresenter.showContextMenuCalls.first) - XCTAssertEqual(menu.items.count, 6) - - menu.performActionForItem(at: 0) - XCTAssertEqual(actionsHandler.openCalls.last, CapturingNewTabPageFavoritesActionsHandler.OpenCall("https://example.com".url!, .newTab)) - - menu.performActionForItem(at: 1) - XCTAssertEqual(actionsHandler.openCalls.last, CapturingNewTabPageFavoritesActionsHandler.OpenCall("https://example.com".url!, .newWindow)) - - menu.performActionForItem(at: 3) - XCTAssertEqual(actionsHandler.editCalls.last, favoritesModel.favorites.first) - - menu.performActionForItem(at: 4) - XCTAssertEqual(actionsHandler.removeFavoriteCalls.last, favoritesModel.favorites.first) - - menu.performActionForItem(at: 5) - XCTAssertEqual(actionsHandler.deleteBookmarkCalls.last, favoritesModel.favorites.first) - } - // MARK: - Helper functions - func handleMessage(named methodName: NewTabPageFavoritesClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { + func handleMessage( + named methodName: NewTabPageFavoritesClientUnderTest.MessageName, + parameters: Any = [], + file: StaticString = #file, + line: UInt = #line + ) async throws -> Response { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) return try XCTUnwrap(response as? Response, file: file, line: line) } - func handleMessageExpectingNilResponse(named methodName: NewTabPageFavoritesClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + func handleMessageExpectingNilResponse( + named methodName: NewTabPageFavoritesClientUnderTest.MessageName, + parameters: Any = [], + file: StaticString = #file, + line: UInt = #line + ) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) XCTAssertNil(response, file: file, line: line) diff --git a/UnitTests/NewTabPage/NewTabPageFavoritesModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift similarity index 66% rename from UnitTests/NewTabPage/NewTabPageFavoritesModelTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift index f2a7109fd6..7785c1928d 100644 --- a/UnitTests/NewTabPage/NewTabPageFavoritesModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift @@ -19,28 +19,32 @@ import Combine import TestUtils import XCTest -@testable import DuckDuckGo_Privacy_Browser +@testable import NewTabPage final class NewTabPageFavoritesModelTests: XCTestCase { - var model: NewTabPageFavoritesModel! - var bookmarkManager: MockBookmarkManager! + var model: NewTabPageFavoritesModel! var settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor! + var favoritesSubject: PassthroughSubject<[MockNewTabPageFavorite], Never>! override func setUp() async throws { try await super.setUp() - settingsPersistor = UserDefaultsNewTabPageFavoritesSettingsPersistor(MockKeyValueStore()) + settingsPersistor = UserDefaultsNewTabPageFavoritesSettingsPersistor(MockKeyValueStore(), getLegacySetting: nil) - bookmarkManager = MockBookmarkManager() - model = NewTabPageFavoritesModel(bookmarkManager: bookmarkManager, settingsPersistor: settingsPersistor) + favoritesSubject = PassthroughSubject<[MockNewTabPageFavorite], Never>() + model = NewTabPageFavoritesModel( + actionsHandler: CapturingNewTabPageFavoritesActionsHandler(), + favoritesPublisher: favoritesSubject.eraseToAnyPublisher(), + settingsPersistor: settingsPersistor + ) } func testWhenBookmarkListIsUpdatedThenFavoritesListIsUpdated() async { - let favorite1 = Bookmark(id: "1", url: "https://example.com/1", title: "1", isFavorite: true) - let favorite2 = Bookmark(id: "2", url: "https://example.com/2", title: "2", isFavorite: true) + let favorite1 = MockNewTabPageFavorite(id: "1", title: "1", url: "https://example.com/1") + let favorite2 = MockNewTabPageFavorite(id: "2", title: "2", url: "https://example.com/2") - var favoritesUpdateEvents = [[Bookmark]]() + var favoritesUpdateEvents = [[MockNewTabPageFavorite]]() var cancellable: AnyCancellable? await withCheckedContinuation { continuation in @@ -51,13 +55,9 @@ final class NewTabPageFavoritesModelTests: XCTestCase { favoritesUpdateEvents.append($0) }) - bookmarkManager.list = BookmarkList() - bookmarkManager.list = BookmarkList(entities: [favorite1], topLevelEntities: [favorite1], favorites: [favorite1]) - bookmarkManager.list = BookmarkList( - entities: [favorite1, favorite2], - topLevelEntities: [favorite1, favorite2], - favorites: [favorite1, favorite2] - ) + favoritesSubject.send([]) + favoritesSubject.send([favorite1]) + favoritesSubject.send([favorite1, favorite2]) } cancellable?.cancel() diff --git a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift index 46c8b78504..5fb0232fb7 100644 --- a/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift +++ b/UnitTests/Bookmarks/Model/LocalBookmarkManagerTests.swift @@ -20,6 +20,7 @@ import Bookmarks import Combine import Foundation import os.log +import Utilities import XCTest @testable import DuckDuckGo_Privacy_Browser diff --git a/UnitTests/Common/Extensions/URLExtensionTests.swift b/UnitTests/Common/Extensions/URLExtensionTests.swift index 402290ecbc..f190681932 100644 --- a/UnitTests/Common/Extensions/URLExtensionTests.swift +++ b/UnitTests/Common/Extensions/URLExtensionTests.swift @@ -239,25 +239,4 @@ final class URLExtensionTests: XCTestCase { let testedURL = URL(string: "https://duckduckgo.com/subscriptions")! XCTAssertFalse(testedURL.isChild(of: parentURL)) } - - // MARK: - Duck Favicon URL - - func testDuckFaviconURL() throws { - XCTAssertEqual( - URL.duckFavicon(for: "https://example.com".url!)?.absoluteString, - "duck://favicon/https%3A//example.com" - ) - XCTAssertEqual( - URL.duckFavicon(for: "https://example.com/1/2/3#anchor".url!)?.absoluteString, - "duck://favicon/https%3A//example.com/1/2/3%23anchor" - ) - XCTAssertEqual( - URL.duckFavicon(for: "https://example.com/1/2/3?query=yes&other=no".url!)?.absoluteString, - "duck://favicon/https%3A//example.com/1/2/3%3Fquery=yes&other=no" - ) - XCTAssertEqual( - URL.duckFavicon(for: "https://рнидс.срб/".url!)?.absoluteString, - "duck://favicon/https%3A//xn--d1aholi.xn--90a3ac/" - ) - } } From f7b854b4485fcfff8de252ebf1c05d36d4bb3e08 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 13:20:54 +0100 Subject: [PATCH 20/62] Move RMF --- DuckDuckGo.xcodeproj/project.pbxproj | 20 ---- DuckDuckGo/Application/AppDelegate.swift | 17 +-- .../ActiveRemoteMessageModel+NewTabPage.swift | 40 +++++++ .../NewTabPageActionsManagerExtension.swift | 5 +- .../ActiveRemoteMessageModel.swift | 14 ++- .../NewTabPage/RMF/NewTabPageRMFClient.swift | 40 +++---- ...ewTabPageActiveRemoteMessageProvider.swift | 45 ++++++++ .../NewTabPageRMFClientTests.swift | 106 ++---------------- .../NewTabPage/NewTabPageTestsHelper.swift | 35 ------ .../ActiveRemoteMessageModelTests.swift | 12 +- 10 files changed, 136 insertions(+), 198 deletions(-) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift rename {UnitTests/NewTabPage => LocalPackages/NewTabPage/Tests/NewTabPageTests}/NewTabPageRMFClientTests.swift (65%) delete mode 100644 UnitTests/NewTabPage/NewTabPageTestsHelper.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f2c768a48e..3390001344 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1168,10 +1168,6 @@ 3767319F2C7F416200EB097B /* CustomBackgroundTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3767319B2C7F415200EB097B /* CustomBackgroundTests.swift */; }; 376731A12C7F50D200EB097B /* Logger+HomePageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */; }; 376731A22C7F50D200EB097B /* Logger+HomePageSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */; }; - 376788122CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */; }; - 376788132CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */; }; - 376788182CED4C4100F59D83 /* NewTabPageTestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */; }; - 376788192CED4C4100F59D83 /* NewTabPageTestsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */; }; 3768D8382C24BFF5004120AE /* RemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3768D8372C24BFF5004120AE /* RemoteMessageView.swift */; }; 3768D8392C24BFF5004120AE /* RemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3768D8372C24BFF5004120AE /* RemoteMessageView.swift */; }; 3768D83B2C24C0A8004120AE /* RemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3768D83A2C24C0A8004120AE /* RemoteMessageViewModel.swift */; }; @@ -3670,8 +3666,6 @@ 376731962C7F36AA00EB097B /* UserBackgroundImageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserBackgroundImageTests.swift; sourceTree = ""; }; 3767319B2C7F415200EB097B /* CustomBackgroundTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomBackgroundTests.swift; sourceTree = ""; }; 376731A02C7F50D200EB097B /* Logger+HomePageSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+HomePageSettings.swift"; sourceTree = ""; }; - 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageRMFClientTests.swift; sourceTree = ""; }; - 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageTestsHelper.swift; sourceTree = ""; }; 3768D8372C24BFF5004120AE /* RemoteMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessageView.swift; sourceTree = ""; }; 3768D83A2C24C0A8004120AE /* RemoteMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteMessageViewModel.swift; sourceTree = ""; }; 3768D83F2C29C1F1004120AE /* ActiveRemoteMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveRemoteMessageModel.swift; sourceTree = ""; }; @@ -5827,15 +5821,6 @@ path = NewTabPage; sourceTree = ""; }; - 3767880A2CECCB6000F59D83 /* NewTabPage */ = { - isa = PBXGroup; - children = ( - 376788172CED4C3A00F59D83 /* NewTabPageTestsHelper.swift */, - 376788112CECF03000F59D83 /* NewTabPageRMFClientTests.swift */, - ); - path = NewTabPage; - sourceTree = ""; - }; 3775913429AB99DA00E26367 /* Sync */ = { isa = PBXGroup; children = ( @@ -8025,7 +8010,6 @@ 378205F9283C275E00D1D4AA /* Menus */, AA91F83627076ED100771A0D /* NavigationBar */, 4BCF15E32ABB987F0083F6DF /* NetworkProtection */, - 3767880A2CECCB6000F59D83 /* NewTabPage */, 85F487B3276A8F1B003CE668 /* Onboarding */, 1D3B1AB7293405F5006F4388 /* PasswordManagers */, B6106BA126A7BE430013B453 /* Permissions */, @@ -12370,7 +12354,6 @@ 3706FE3F293F661700E42796 /* FileStoreMock.swift in Sources */, B6619F042B17123200CD9186 /* DataImportViewModelTests.swift in Sources */, 1D8C2FE62B70F4C4005E4BBD /* TabSnapshotExtensionTests.swift in Sources */, - 376788192CED4C4100F59D83 /* NewTabPageTestsHelper.swift in Sources */, 3706FE40293F661700E42796 /* BWResponseTests.swift in Sources */, 3706FE41293F661700E42796 /* DownloadListCoordinatorTests.swift in Sources */, 986189E72A7CFB3E001B4519 /* LocalBookmarkStoreSavingTests.swift in Sources */, @@ -12451,7 +12434,6 @@ 3706FE6A293F661700E42796 /* FirefoxKeyReaderTests.swift in Sources */, 1DA860732BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 3706FE6B293F661700E42796 /* AppKitPrivateMethodsAvailabilityTests.swift in Sources */, - 376788132CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */, 3706FE6D293F661700E42796 /* ChromiumBookmarksReaderTests.swift in Sources */, C1E961F42B87B276001760E1 /* MockAutofillActionPresenter.swift in Sources */, 567A23DC2C88980B0010F66C /* ContextualDaxDialogsFactoryTests.swift in Sources */, @@ -14123,7 +14105,6 @@ 4B70C00227B0793D000386ED /* CrashReportTests.swift in Sources */, B6656E0D2B29C733008798A1 /* FileImportViewLocalizationTests.swift in Sources */, 37D23787287F5C2900BCE03B /* PinnedTabsViewModelTests.swift in Sources */, - 376788182CED4C4100F59D83 /* NewTabPageTestsHelper.swift in Sources */, 4B9DB0542A983B55000927DB /* MockWaitlistStorage.swift in Sources */, 37E260922C8A3EB4006EE07F /* MockHomePageSettingsModelNavigator.swift in Sources */, 4BF4EA5027C71F26004E57C4 /* PasswordManagementListSectionTests.swift in Sources */, @@ -14192,7 +14173,6 @@ 560C6EDB2CCA6D5200D411E2 /* OnboardingFireButtonDialogViewModelTests.swift in Sources */, 560C6EC92CCA381A00D411E2 /* OnboardingPixelReporterTests.swift in Sources */, 1E2BEAE42C8B00B5002741A3 /* SubscriptionPagesUseSubscriptionFeatureTests.swift in Sources */, - 376788122CECF03200F59D83 /* NewTabPageRMFClientTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DuckDuckGo/Application/AppDelegate.swift b/DuckDuckGo/Application/AppDelegate.swift index a028e5de96..85b5deeb90 100644 --- a/DuckDuckGo/Application/AppDelegate.swift +++ b/DuckDuckGo/Application/AppDelegate.swift @@ -97,12 +97,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { private(set) lazy var newTabPageActionsManager: NewTabPageActionsManaging = NewTabPageActionsManager( appearancePreferences: .shared, activeRemoteMessageModel: activeRemoteMessageModel, - privacyStats: privacyStats, - openURLHandler: { url in - Task { @MainActor in - WindowControllersManager.shared.showTab(with: .contentFromURL(url, source: .appOpenUrl)) - } - } + privacyStats: privacyStats ) let privacyStats: PrivacyStatsCollecting let activeRemoteMessageModel: ActiveRemoteMessageModel @@ -267,13 +262,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate { privacyConfigurationManager: ContentBlocking.shared.privacyConfigurationManager ) ) - activeRemoteMessageModel = ActiveRemoteMessageModel(remoteMessagingClient: remoteMessagingClient) + activeRemoteMessageModel = ActiveRemoteMessageModel(remoteMessagingClient: remoteMessagingClient, openURLHandler: { url in + WindowControllersManager.shared.showTab(with: .contentFromURL(url, source: .appOpenUrl)) + }) } else { // As long as remoteMessagingClient is private to App Delegate and activeRemoteMessageModel // is used only by HomePage RootView as environment object, // it's safe to not initialize the client for unit tests to avoid side effects. remoteMessagingClient = nil - activeRemoteMessageModel = ActiveRemoteMessageModel(remoteMessagingStore: nil, remoteMessagingAvailabilityProvider: nil) + activeRemoteMessageModel = ActiveRemoteMessageModel( + remoteMessagingStore: nil, + remoteMessagingAvailabilityProvider: nil, + openURLHandler: { _ in } + ) } featureFlagger = DefaultFeatureFlagger( diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift index b4f4d8684c..26b617934d 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -24,4 +24,44 @@ extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { var remoteMessagePublisher: AnyPublisher { $remoteMessage.dropFirst().eraseToAnyPublisher() } + + func isMessageSupported(_ message: RemoteMessageModel) -> Bool { + return message.content?.isSupported == true + } + + func handleAction(_ action: RemoteAction?, andDismissUsing button: RemoteMessageButton) async { + if let action { + await handleAction(action) + } + await dismissRemoteMessage(with: .init(button)) + } + + private func handleAction(_ remoteAction: RemoteAction) async { + switch remoteAction { + case .url(let value), .share(let value, _), .survey(let value): + if let url = URL.makeURL(from: value) { + await openURLHandler(url) + } + case .appStore: + await openURLHandler(.appStore) + default: + break + } + } +} + +extension RemoteMessageViewModel.ButtonAction { + + init(_ button: RemoteMessageButton) { + switch button { + case .close: + self = .close + case .action: + self = .action + case .primaryAction: + self = .primaryAction + case .secondaryAction: + self = .secondaryAction + } + } } diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 63549e46dd..4345026632 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -25,8 +25,7 @@ extension NewTabPageActionsManager { convenience init( appearancePreferences: AppearancePreferences, activeRemoteMessageModel: ActiveRemoteMessageModel, - privacyStats: PrivacyStatsCollecting, - openURLHandler: @escaping (URL) -> Void + privacyStats: PrivacyStatsCollecting ) { let privacyStatsModel = NewTabPagePrivacyStatsModel( privacyStats: privacyStats, @@ -43,7 +42,7 @@ extension NewTabPageActionsManager { self.init(scriptClients: [ NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences), - NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel, openURLHandler: openURLHandler), + NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), NewTabPageFavoritesClient(favoritesModel: favoritesModel), NewTabPagePrivacyStatsClient(model: privacyStatsModel) diff --git a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift index 757c209c62..0af0f67f97 100644 --- a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift +++ b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift @@ -47,10 +47,16 @@ final class ActiveRemoteMessageModel: ObservableObject { */ let store: () -> RemoteMessagingStoring? - convenience init(remoteMessagingClient: RemoteMessagingClient) { + /** + * Handler for opening URLs for Remote Messages displayed on HTML New Tab Page + */ + let openURLHandler: (URL) async -> Void + + convenience init(remoteMessagingClient: RemoteMessagingClient, openURLHandler: @escaping (URL) async -> Void) { self.init( remoteMessagingStore: remoteMessagingClient.store, - remoteMessagingAvailabilityProvider: remoteMessagingClient.remoteMessagingAvailabilityProvider + remoteMessagingAvailabilityProvider: remoteMessagingClient.remoteMessagingAvailabilityProvider, + openURLHandler: openURLHandler ) } @@ -59,9 +65,11 @@ final class ActiveRemoteMessageModel: ObservableObject { */ init( remoteMessagingStore: @escaping @autoclosure () -> RemoteMessagingStoring?, - remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding? + remoteMessagingAvailabilityProvider: RemoteMessagingAvailabilityProviding?, + openURLHandler: @escaping (URL) async -> Void ) { self.store = remoteMessagingStore + self.openURLHandler = openURLHandler let messagesDidChangePublisher = NotificationCenter.default.publisher(for: RemoteMessagingStore.Notifications.remoteMessagesDidChange) .asVoid() diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift index ace1515a89..9132783890 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift @@ -26,7 +26,13 @@ public protocol NewTabPageActiveRemoteMessageProviding { var remoteMessage: RemoteMessageModel? { get set } var remoteMessagePublisher: AnyPublisher { get } - func dismissRemoteMessage(with action: RemoteMessageViewModel.ButtonAction?) async + func isMessageSupported(_ message: RemoteMessageModel) -> Bool + + func handleAction(_ action: RemoteAction?, andDismissUsing button: RemoteMessageButton) async +} + +public enum RemoteMessageButton: Equatable { + case close, action, primaryAction, secondaryAction } public final class NewTabPageRMFClient: NewTabPageScriptClient { @@ -34,12 +40,10 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { public let remoteMessageProvider: NewTabPageActiveRemoteMessageProviding public weak var userScriptsSource: NewTabPageUserScriptsSource? - private let openURLHandler: (URL) -> Void private var cancellables = Set() - public init(remoteMessageProvider: NewTabPageActiveRemoteMessageProviding, openURLHandler: @escaping (URL) -> Void) { + public init(remoteMessageProvider: NewTabPageActiveRemoteMessageProviding) { self.remoteMessageProvider = remoteMessageProvider - self.openURLHandler = openURLHandler remoteMessageProvider.remoteMessagePublisher .sink { [weak self] remoteMessage in @@ -81,7 +85,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { return nil } - await remoteMessageProvider.dismissRemoteMessage(with: .close) + await remoteMessageProvider.handleAction(nil, andDismissUsing: .close) return nil } @@ -94,11 +98,9 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { switch remoteMessageProvider.remoteMessage?.content { case let .bigSingleAction(_, _, _, _, primaryAction): - handleAction(remoteAction: primaryAction) - await remoteMessageProvider.dismissRemoteMessage(with: .action) + await remoteMessageProvider.handleAction(primaryAction, andDismissUsing: .action) case let .bigTwoAction(_, _, _, _, primaryAction, _, _): - handleAction(remoteAction: primaryAction) - await remoteMessageProvider.dismissRemoteMessage(with: .primaryAction) + await remoteMessageProvider.handleAction(primaryAction, andDismissUsing: .primaryAction) default: break } @@ -114,30 +116,16 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { switch remoteMessageProvider.remoteMessage?.content { case let .bigTwoAction(_, _, _, _, _, _, secondaryAction): - handleAction(remoteAction: secondaryAction) - await remoteMessageProvider.dismissRemoteMessage(with: .secondaryAction) + await remoteMessageProvider.handleAction(secondaryAction, andDismissUsing: .secondaryAction) default: break } return nil } - private func handleAction(remoteAction: RemoteAction) { - switch remoteAction { - case .url(let value), .share(let value, _), .survey(let value): - if let url = URL.makeURL(from: value) { - openURLHandler(url) - } - case .appStore: - openURLHandler(.appStore) - default: - break - } - } - private func notifyRemoteMessageDidChange(_ remoteMessage: RemoteMessageModel?) { let data: NewTabPageUserScript.RMFData = { - guard let remoteMessage else { + guard let remoteMessage, remoteMessageProvider.isMessageSupported(remoteMessage) else { return .init(content: nil) } return .init(content: NewTabPageUserScript.RMFMessage(remoteMessage)) @@ -178,7 +166,7 @@ public extension NewTabPageUserScript { } public init?(_ remoteMessageModel: RemoteMessageModel) { - guard let modelType = remoteMessageModel.content, modelType.isSupported else { + guard let modelType = remoteMessageModel.content else { return nil } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift new file mode 100644 index 0000000000..1cd2f3ccd1 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageActiveRemoteMessageProvider.swift @@ -0,0 +1,45 @@ +// +// CapturingNewTabPageActiveRemoteMessageProvider.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 RemoteMessaging +import XCTest +import NewTabPage + +final class CapturingNewTabPageActiveRemoteMessageProvider: NewTabPageActiveRemoteMessageProviding { + @Published var remoteMessage: RemoteMessageModel? + + var remoteMessagePublisher: AnyPublisher { + $remoteMessage.dropFirst().eraseToAnyPublisher() + } + + func isMessageSupported(_ message: RemoteMessageModel) -> Bool { + true + } + + func handleAction(_ action: RemoteAction?, andDismissUsing button: RemoteMessageButton) async { + dismissCalls.append(.init(action: action, button: button)) + } + + struct Dismiss: Equatable { + let action: RemoteAction? + let button: RemoteMessageButton + } + + var dismissCalls: [Dismiss] = [] +} diff --git a/UnitTests/NewTabPage/NewTabPageRMFClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift similarity index 65% rename from UnitTests/NewTabPage/NewTabPageRMFClientTests.swift rename to LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift index 11d1cd8489..ae271591b3 100644 --- a/UnitTests/NewTabPage/NewTabPageRMFClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift @@ -17,39 +17,19 @@ // import Combine -import NewTabPage import RemoteMessaging import XCTest -@testable import DuckDuckGo_Privacy_Browser - -final class CapturingNewTabPageActiveRemoteMessageProvider: NewTabPageActiveRemoteMessageProviding { - @Published var remoteMessage: RemoteMessageModel? - - var remoteMessagePublisher: AnyPublisher { - $remoteMessage.dropFirst().eraseToAnyPublisher() - } - - func dismissRemoteMessage(with action: RemoteMessageViewModel.ButtonAction?) async { - dismissCalls.append(action) - } - - var dismissCalls: [RemoteMessageViewModel.ButtonAction?] = [] -} +@testable import NewTabPage final class NewTabPageRMFClientTests: XCTestCase { var client: NewTabPageRMFClient! var remoteMessageProvider: CapturingNewTabPageActiveRemoteMessageProvider! - var openURLCalls: [URL] = [] var userScript: NewTabPageUserScript! override func setUpWithError() throws { try super.setUpWithError() - openURLCalls = [] remoteMessageProvider = CapturingNewTabPageActiveRemoteMessageProvider() - client = NewTabPageRMFClient( - remoteMessageProvider: remoteMessageProvider, - openURLHandler: { [weak self] in self?.openURLCalls.append($0) } - ) + client = NewTabPageRMFClient(remoteMessageProvider: remoteMessageProvider) userScript = NewTabPageUserScript() client.registerMessageHandlers(for: userScript) } @@ -113,7 +93,7 @@ final class NewTabPageRMFClientTests: XCTestCase { let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) - XCTAssertEqual(remoteMessageProvider.dismissCalls, [.close]) + XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: nil, button: .close)]) } func testWhenMessageIdDoesNotMatchThenDismissHasNoEffect() async throws { @@ -131,7 +111,7 @@ final class NewTabPageRMFClientTests: XCTestCase { let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(remoteMessageProvider.dismissCalls, [.action]) + XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .action)]) } func testWhenTwoActionMessageThenPrimaryActionSendsPrimaryActionToProvider() async throws { @@ -139,7 +119,7 @@ final class NewTabPageRMFClientTests: XCTestCase { let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(remoteMessageProvider.dismissCalls, [.primaryAction]) + XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .primaryAction)]) } func testWhenMessageHasNoButtonThenPrimaryActionHasNoEffect() async throws { @@ -158,54 +138,6 @@ final class NewTabPageRMFClientTests: XCTestCase { XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } - func testWhenSingleActionMessageThenPrimaryActionWithAppStoreOpensAppStoreURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, [.appStore]) - } - - func testWhenSingleActionMessageThenPrimaryActionWithURLOpensURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .url(value: "http://example.com")) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, ["http://example.com".url!]) - } - - func testWhenSingleActionMessageThenPrimaryActionWithSurveyOpensSurveyURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .survey(value: "http://example.com")) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, ["http://example.com".url!]) - } - - func testWhenTwoActionMessageThenPrimaryActionWithAppStoreOpensAppStoreURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, [.appStore]) - } - - func testWhenTwoActionMessageThenPrimaryActionWithURLOpensURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .url(value: "http://example.com"), secondaryAction: .dismiss) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, ["http://example.com".url!]) - } - - func testWhenTwoActionMessageThenPrimaryActionWithSurveyOpensSurveyURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .survey(value: "http://example.com"), secondaryAction: .dismiss) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, ["http://example.com".url!]) - } - // MARK: - secondaryAction func testWhenTwoActionMessageThenSecondaryActionSendsSecondaryActionToProvider() async throws { @@ -213,7 +145,7 @@ final class NewTabPageRMFClientTests: XCTestCase { let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) - XCTAssertEqual(remoteMessageProvider.dismissCalls, [.secondaryAction]) + XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .secondaryAction)]) } func testWhenSingleActionMessageThenSecondaryActionHasNoEffect() async throws { @@ -237,31 +169,7 @@ final class NewTabPageRMFClientTests: XCTestCase { let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) - XCTAssertTrue(remoteMessageProvider.dismissCalls.isEmpty) - } - - func testWhenTwoActionMessageThenSecondaryActionWithAppStoreOpensAppStoreURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .dismiss, secondaryAction: .appStore) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, [.appStore]) - } - - func testWhenTwoActionMessageThenSecondaryActionWithURLOpensURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .url(value: "http://example.com")) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, ["http://example.com".url!]) - } - - func testWhenTwoActionMessageThenSecondaryActionWithSurveyOpensSurveyURL() async throws { - remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .survey(value: "http://example.com")) - - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) - XCTAssertEqual(openURLCalls, ["http://example.com".url!]) + XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } // MARK: - Helper functions diff --git a/UnitTests/NewTabPage/NewTabPageTestsHelper.swift b/UnitTests/NewTabPage/NewTabPageTestsHelper.swift deleted file mode 100644 index abf110eb36..0000000000 --- a/UnitTests/NewTabPage/NewTabPageTestsHelper.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// NewTabPageTestsHelper.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 XCTest - -enum NewTabPageTestsHelper { - - static func asJSON(_ value: Any, file: StaticString = #file, line: UInt = #line) throws -> Any { - if JSONSerialization.isValidJSONObject(value) { - return value - } - if let encodableValue = value as? Encodable { - let jsonData = try JSONEncoder().encode(encodableValue) - return try JSONSerialization.jsonObject(with: jsonData) - } - XCTFail("invalid JSON value", file: file, line: line) - return [] - } -} diff --git a/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift b/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift index 44631d4a62..ed21ad5d34 100644 --- a/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift +++ b/UnitTests/RemoteMessaging/ActiveRemoteMessageModelTests.swift @@ -39,7 +39,8 @@ final class ActiveRemoteMessageModelTests: XCTestCase { store.scheduledRemoteMessage = nil model = ActiveRemoteMessageModel( remoteMessagingStore: self.store, - remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider() + remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider(), + openURLHandler: { _ in } ) XCTAssertNil(model.remoteMessage) @@ -49,7 +50,8 @@ final class ActiveRemoteMessageModelTests: XCTestCase { store.scheduledRemoteMessage = message model = ActiveRemoteMessageModel( remoteMessagingStore: self.store, - remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider() + remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider(), + openURLHandler: { _ in } ) XCTAssertEqual(model.remoteMessage, message) @@ -59,7 +61,8 @@ final class ActiveRemoteMessageModelTests: XCTestCase { store.scheduledRemoteMessage = message model = ActiveRemoteMessageModel( remoteMessagingStore: self.store, - remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider() + remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider(), + openURLHandler: { _ in } ) await model.dismissRemoteMessage(with: .close) @@ -70,7 +73,8 @@ final class ActiveRemoteMessageModelTests: XCTestCase { store.scheduledRemoteMessage = message model = ActiveRemoteMessageModel( remoteMessagingStore: self.store, - remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider() + remoteMessagingAvailabilityProvider: MockRemoteMessagingAvailabilityProvider(), + openURLHandler: { _ in } ) XCTAssertFalse(store.hasShownRemoteMessage(withID: message.id)) From 76f176edba2122bdc5c348db4553e4ddf26ea10e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 13:21:21 +0100 Subject: [PATCH 21/62] Update project file --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3390001344..91cf6ee97a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5814,9 +5814,9 @@ 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */, 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */, 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */, + 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */, 371BBC5D2D00CA3E008FA0C7 /* NewTabPageWebViewModel.swift */, 371BBC5A2D00C911008FA0C7 /* NewTabPageActionsManagerExtension.swift */, - 3772BEDC2D019CDF0019B9EF /* DefaultsFavoritesActionHandler.swift */, ); path = NewTabPage; sourceTree = ""; From bc303e0380370a644a81b6bf4d645bf90a9c05ee Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 13:41:29 +0100 Subject: [PATCH 22/62] Fix SwiftLint violations and compilation failures --- .../ContinueSetUpModel+NewTabPage.swift | 1 - .../NewTabPageActionsManagerExtension.swift | 2 +- LocalPackages/NewTabPage/Package.swift | 2 +- .../Favorites/NewTabPageFavoritesClient.swift | 13 ++++++------ .../NewTabPageTests/DuckFaviconURLTests.swift | 1 - .../NewTabPageFavoritesClientTests.swift | 21 ++++--------------- LocalPackages/WebKitExtensions/Package.swift | 2 +- 7 files changed, 14 insertions(+), 28 deletions(-) diff --git a/DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift index 2b1487d626..ac3f65912b 100644 --- a/DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ContinueSetUpModel+NewTabPage.swift @@ -101,4 +101,3 @@ extension NewTabPageNextStepsCardsClient.CardID { } } } - diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 4345026632..4d92b5ecc0 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -44,7 +44,7 @@ extension NewTabPageActionsManager { NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), - NewTabPageFavoritesClient(favoritesModel: favoritesModel), + NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: Int(Favicon.SizeCategory.medium.rawValue)), NewTabPagePrivacyStatsClient(model: privacyStatsModel) ]) } diff --git a/LocalPackages/NewTabPage/Package.swift b/LocalPackages/NewTabPage/Package.swift index 376b5ba786..967a7e4d95 100644 --- a/LocalPackages/NewTabPage/Package.swift +++ b/LocalPackages/NewTabPage/Package.swift @@ -32,7 +32,7 @@ let package = Package( targets: ["NewTabPage"]), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.2"), .package(path: "../WebKitExtensions"), .package(path: "../Utilities"), ], diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift index 282a5133c3..1d2995560c 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift @@ -29,9 +29,11 @@ public final class NewTabPageFavoritesClient: NewTa public let favoritesModel: NewTabPageFavoritesModel public weak var userScriptsSource: NewTabPageUserScriptsSource? private var cancellables: Set = [] + private let preferredFaviconSize: Int - public init(favoritesModel: NewTabPageFavoritesModel) { + public init(favoritesModel: NewTabPageFavoritesModel, preferredFaviconSize: Int) { self.favoritesModel = favoritesModel + self.preferredFaviconSize = preferredFaviconSize favoritesModel.$favorites.dropFirst() .sink { [weak self] favorites in @@ -96,7 +98,7 @@ public final class NewTabPageFavoritesClient: NewTa @MainActor public func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let favorites = favoritesModel.favorites.map { - NewTabPageFavoritesClient.Favorite($0, onFaviconMissing: favoritesModel.onFaviconMissing) + NewTabPageFavoritesClient.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) } return NewTabPageFavoritesClient.FavoritesData(favorites: favorites) } @@ -104,7 +106,7 @@ public final class NewTabPageFavoritesClient: NewTa @MainActor private func notifyDataUpdated(_ favorites: [NewTabPageFavorite]) { let favorites = favoritesModel.favorites.map { - NewTabPageFavoritesClient.Favorite($0, onFaviconMissing: favoritesModel.onFaviconMissing) + NewTabPageFavoritesClient.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) } pushMessage(named: MessageName.onDataUpdate.rawValue, params: NewTabPageFavoritesClient.FavoritesData(favorites: favorites)) } @@ -210,14 +212,13 @@ public extension NewTabPageFavoritesClient { } @MainActor - init(_ bookmark: NewTabPageFavorite, onFaviconMissing: () -> Void) { + init(_ bookmark: NewTabPageFavorite, preferredFaviconSize: Int, onFaviconMissing: () -> Void) { id = bookmark.id title = bookmark.title url = bookmark.url if let url = bookmark.urlObject, let duckFaviconURL = URL.duckFavicon(for: url) { - // TODO: Int(Favicon.SizeCategory.medium.rawValue) - favicon = FavoriteFavicon(maxAvailableSize: 132, src: duckFaviconURL.absoluteString) + favicon = FavoriteFavicon(maxAvailableSize: preferredFaviconSize, src: duckFaviconURL.absoluteString) } else { onFaviconMissing() favicon = nil diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift index c4e965eb90..8c8e669c76 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/DuckFaviconURLTests.swift @@ -22,7 +22,6 @@ import Combine final class DuckFaviconURLTests: XCTestCase { - func testDuckFaviconURL() throws { XCTAssertEqual( URL.duckFavicon(for: URL(string: "https://example.com")!)?.absoluteString, diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index 4c466c1b6a..b4be5d3bfe 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -17,7 +17,6 @@ // import Combine -import NewTabPage import RemoteMessaging import TestUtils import XCTest @@ -37,14 +36,14 @@ final class NewTabPageFavoritesClientTests: XCTestCase { try super.setUpWithError() contextMenuPresenter = CapturingNewTabPageContextMenuPresenter() actionsHandler = CapturingNewTabPageFavoritesActionsHandler() - favoritesModel = NewTabPageFavoritesModel.init( + favoritesModel = NewTabPageFavoritesModel( actionsHandler: actionsHandler, favoritesPublisher: Empty().eraseToAnyPublisher(), contextMenuPresenter: contextMenuPresenter, settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor(MockKeyValueStore(), getLegacySetting: nil) ) - client = NewTabPageFavoritesClient(favoritesModel: favoritesModel) + client = NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: 100) userScript = NewTabPageUserScript() client.registerMessageHandlers(for: userScript) @@ -161,25 +160,13 @@ final class NewTabPageFavoritesClientTests: XCTestCase { // MARK: - Helper functions - func handleMessage( - named methodName: NewTabPageFavoritesClientUnderTest.MessageName, - parameters: Any = [], - file: StaticString = #file, - line: UInt = #line - ) async throws -> Response { - + func handleMessage(named methodName: NewTabPageFavoritesClientUnderTest.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) return try XCTUnwrap(response as? Response, file: file, line: line) } - func handleMessageExpectingNilResponse( - named methodName: NewTabPageFavoritesClientUnderTest.MessageName, - parameters: Any = [], - file: StaticString = #file, - line: UInt = #line - ) async throws { - + func handleMessageExpectingNilResponse(named methodName: NewTabPageFavoritesClientUnderTest.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) XCTAssertNil(response, file: file, line: line) diff --git a/LocalPackages/WebKitExtensions/Package.swift b/LocalPackages/WebKitExtensions/Package.swift index 11a89450c4..bc9339cedd 100644 --- a/LocalPackages/WebKitExtensions/Package.swift +++ b/LocalPackages/WebKitExtensions/Package.swift @@ -32,7 +32,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.1"), + .package(url: "https://github.com/duckduckgo/BrowserServicesKit", exact: "217.0.2"), .package(path: "../AppKitExtensions") ], targets: [ From ad9850d78726d5c8c75ee7a9fb28360f9d037fc0 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 13:57:49 +0100 Subject: [PATCH 23/62] Fix testThatGetDataReturnsFavoritesFromTheModel --- .../NewTabPageFavoritesClientTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index b4be5d3bfe..d3cdba7267 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -100,11 +100,11 @@ final class NewTabPageFavoritesClientTests: XCTestCase { ] let data: NewTabPageFavoritesClientUnderTest.FavoritesData = try await handleMessage(named: .getData) XCTAssertEqual(data.favorites, [ - .init(id: "1", title: "A", url: "https://a.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//a.com")), - .init(id: "10", title: "B", url: "https://b.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//b.com")), - .init(id: "5", title: "C", url: "https://c.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//c.com")), - .init(id: "2", title: "D", url: "https://d.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//d.com")), - .init(id: "3", title: "E", url: "https://e.com", favicon: .init(maxAvailableSize: 132, src: "duck://favicon/https%3A//e.com")) + .init(id: "1", title: "A", url: "https://a.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//a.com")), + .init(id: "10", title: "B", url: "https://b.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//b.com")), + .init(id: "5", title: "C", url: "https://c.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//c.com")), + .init(id: "2", title: "D", url: "https://d.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//d.com")), + .init(id: "3", title: "E", url: "https://e.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//e.com")) ]) } From 1d82c3ba21acc92e1e265d12ba5f55ad42d989c6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 14:36:52 +0100 Subject: [PATCH 24/62] Adjust symbols visibility --- .../Favorites/NewTabPageFavoritesClient.swift | 76 +++++++------------ .../Favorites/NewTabPageFavoritesModel.swift | 25 +++--- .../NewTabPageConfigurationClient.swift | 49 +++++------- .../NewTabPageUserContentController.swift | 12 +-- .../NewTabPage/NewTabPageUserScript.swift | 33 +++----- .../NewTabPageNextStepsCardsClient.swift | 30 +++----- .../NewTabPagePrivacyStatsClient.swift | 32 +++----- .../NewTabPagePrivacyStatsModel.swift | 16 ++-- ...wTabPageActiveRemoteMessageProviding.swift | 29 +++++++ .../NewTabPage/RMF/NewTabPageRMFClient.swift | 71 ++++++++--------- ...ringNewTabPageNextStepsCardsProvider.swift | 52 +++++++++++++ .../NewTabPageNextStepsCardsClientTests.swift | 68 +++++------------ 12 files changed, 241 insertions(+), 252 deletions(-) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift index 1d2995560c..7aa3c1e977 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift @@ -26,8 +26,9 @@ public final class NewTabPageFavoritesClient: NewTa ActionHandler: FavoritesActionsHandling, ActionHandler.FavoriteType == FavoriteType { - public let favoritesModel: NewTabPageFavoritesModel + let favoritesModel: NewTabPageFavoritesModel public weak var userScriptsSource: NewTabPageUserScriptsSource? + private var cancellables: Set = [] private let preferredFaviconSize: Int @@ -52,7 +53,7 @@ public final class NewTabPageFavoritesClient: NewTa .store(in: &cancellables) } - public enum MessageName: String, CaseIterable { + enum MessageName: String, CaseIterable { case add = "favorites_add" case getConfig = "favorites_getConfig" case getData = "favorites_getData" @@ -76,18 +77,18 @@ public final class NewTabPageFavoritesClient: NewTa ]) } - public func add(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func add(params: Any, original: WKScriptMessage) async throws -> Encodable? { await favoritesModel.addNew() return nil } - public func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = favoritesModel.isViewExpanded ? .expanded : .collapsed return NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) } @MainActor - public func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { return nil } @@ -96,7 +97,7 @@ public final class NewTabPageFavoritesClient: NewTa } @MainActor - public func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let favorites = favoritesModel.favorites.map { NewTabPageFavoritesClient.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) } @@ -119,7 +120,7 @@ public final class NewTabPageFavoritesClient: NewTa } @MainActor - public func move(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func move(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let action: NewTabPageFavoritesClient.FavoritesMoveAction = DecodableHelper.decode(from: params) else { return nil } @@ -128,7 +129,7 @@ public final class NewTabPageFavoritesClient: NewTa } @MainActor - public func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let action: NewTabPageFavoritesClient.FavoritesOpenAction = DecodableHelper.decode(from: params) else { return nil } @@ -137,7 +138,7 @@ public final class NewTabPageFavoritesClient: NewTa } @MainActor - public func openContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func openContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let contextMenuAction: NewTabPageFavoritesClient.FavoritesContextMenuAction = DecodableHelper.decode(from: params) else { return nil } @@ -149,62 +150,39 @@ public final class NewTabPageFavoritesClient: NewTa public extension NewTabPageFavoritesClient { struct FavoritesContextMenuAction: Codable { - public let id: String - - public init(id: String) { - self.id = id - } + let id: String } struct FavoritesOpenAction: Codable { - public let id: String - public let url: String - - public init(id: String, url: String) { - self.id = id - self.url = url - } + let id: String + let url: String } struct FavoritesMoveAction: Codable { - public let id: String - public let fromIndex: Int - public let targetIndex: Int - - public init(id: String, fromIndex: Int, targetIndex: Int) { - self.id = id - self.fromIndex = fromIndex - self.targetIndex = targetIndex - } + let id: String + let fromIndex: Int + let targetIndex: Int } struct FavoritesConfig: Codable { - public let expansion: Expansion + let expansion: Expansion - public init(expansion: Expansion) { - self.expansion = expansion - } - - public enum Expansion: String, Codable { + enum Expansion: String, Codable { case expanded, collapsed } } struct FavoritesData: Encodable { - public let favorites: [Favorite] - - public init(favorites: [Favorite]) { - self.favorites = favorites - } + let favorites: [Favorite] } struct Favorite: Encodable, Equatable { - public let favicon: FavoriteFavicon? - public let id: String - public let title: String - public let url: String + let favicon: FavoriteFavicon? + let id: String + let title: String + let url: String - public init(id: String, title: String, url: String, favicon: NewTabPageFavoritesClient.FavoriteFavicon? = nil) { + init(id: String, title: String, url: String, favicon: NewTabPageFavoritesClient.FavoriteFavicon? = nil) { self.id = id self.title = title self.url = url @@ -227,10 +205,10 @@ public extension NewTabPageFavoritesClient { } struct FavoriteFavicon: Encodable, Equatable { - public let maxAvailableSize: Int - public let src: String + let maxAvailableSize: Int + let src: String - public init(maxAvailableSize: Int, src: String) { + init(maxAvailableSize: Int, src: String) { self.maxAvailableSize = maxAvailableSize self.src = src } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift index bed102fb47..2502d3dab2 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesModel.swift @@ -21,23 +21,23 @@ import Combine import Foundation import Persistence -public protocol NewTabPageFavoritesSettingsPersistor: AnyObject { +protocol NewTabPageFavoritesSettingsPersistor: AnyObject { var isViewExpanded: Bool { get set } } -public final class UserDefaultsNewTabPageFavoritesSettingsPersistor: NewTabPageFavoritesSettingsPersistor { +final class UserDefaultsNewTabPageFavoritesSettingsPersistor: NewTabPageFavoritesSettingsPersistor { enum Keys { static let isViewExpanded = "new-tab-page.favorites.is-view-expanded" } private let keyValueStore: KeyValueStoring - public init(_ keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacySetting: @autoclosure () -> Bool?) { + init(_ keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacySetting: @autoclosure () -> Bool?) { self.keyValueStore = keyValueStore migrateFromLegacyHomePageSettings(using: getLegacySetting) } - public var isViewExpanded: Bool { + var isViewExpanded: Bool { get { return keyValueStore.object(forKey: Keys.isViewExpanded) as? Bool ?? false } set { keyValueStore.set(newValue, forKey: Keys.isViewExpanded) } } @@ -78,7 +78,7 @@ public final class NewTabPageFavoritesModel: NSObje ) } - public init( + init( actionsHandler: ActionHandler, favoritesPublisher: AnyPublisher<[FavoriteType], Never>, contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), @@ -100,41 +100,42 @@ public final class NewTabPageFavoritesModel: NSObje .store(in: &cancellables) } - @Published public var isViewExpanded: Bool { + @Published var isViewExpanded: Bool { didSet { settingsPersistor.isViewExpanded = self.isViewExpanded } } - @Published public var favorites: [FavoriteType] = [] + @Published var favorites: [FavoriteType] = [] // MARK: - Actions @MainActor - public func openFavorite(withURL url: String) { + func openFavorite(withURL url: String) { guard let url = URL(string: url), url.isValid else { return } actionsHandler.open(url, target: .current) } - public func moveFavorite(withID bookmarkID: String, fromIndex: Int, toIndex index: Int) { + @MainActor + func moveFavorite(withID bookmarkID: String, fromIndex: Int, toIndex index: Int) { let targetIndex = index > fromIndex ? index + 1 : index actionsHandler.move(bookmarkID, toIndex: targetIndex) } @MainActor - public func addNew() { + func addNew() { actionsHandler.addNewFavorite() } @MainActor - public func onFaviconMissing() { + func onFaviconMissing() { actionsHandler.onFaviconMissing() } // MARK: Context Menu @MainActor - public func showContextMenu(for bookmarkID: String) { + func showContextMenu(for bookmarkID: String) { /** * This isn't very effective (may need to traverse up to entire array) * but it's only ever needed for context menus. I decided to skip diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index e104da11a8..fa7911b433 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -54,7 +54,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { .store(in: &cancellables) } - public enum MessageName: String, CaseIterable { + enum MessageName: String, CaseIterable { case contextMenu case initialSetup case reportInitException @@ -176,62 +176,53 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { } } -public extension NewTabPageUserScript { +extension NewTabPageUserScript { enum WidgetId: String, Codable { case rmf, nextSteps, favorites, privacyStats } struct ContextMenuParams: Codable { - public let visibilityMenuItems: [ContextMenuItem] + let visibilityMenuItems: [ContextMenuItem] - public init(visibilityMenuItems: [ContextMenuItem]) { - self.visibilityMenuItems = visibilityMenuItems - } - - public struct ContextMenuItem: Codable { - public let id: WidgetId - public let title: String - - public init(id: WidgetId, title: String) { - self.id = id - self.title = title - } + struct ContextMenuItem: Codable { + let id: WidgetId + let title: String } } struct NewTabPageConfiguration: Encodable { - public var widgets: [Widget] - public var widgetConfigs: [WidgetConfig] - public var env: String - public var locale: String - public var platform: Platform + var widgets: [Widget] + var widgetConfigs: [WidgetConfig] + var env: String + var locale: String + var platform: Platform - public struct Widget: Encodable, Equatable { + struct Widget: Encodable, Equatable { public var id: WidgetId } - public struct WidgetConfig: Codable, Equatable { + struct WidgetConfig: Codable, Equatable { - public enum WidgetVisibility: String, Codable { + enum WidgetVisibility: String, Codable { case visible, hidden - public var isVisible: Bool { + var isVisible: Bool { self == .visible } } - public init(id: WidgetId, isVisible: Bool) { + init(id: WidgetId, isVisible: Bool) { self.id = id self.visibility = isVisible ? .visible : .hidden } - public var id: WidgetId - public var visibility: WidgetVisibility + var id: WidgetId + var visibility: WidgetVisibility } - public struct Platform: Encodable, Equatable { - public var name: String + struct Platform: Encodable, Equatable { + var name: String } } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift index 4d8f732d73..f27ec85d28 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserContentController.swift @@ -24,7 +24,7 @@ import WebKitExtensions public final class NewTabPageUserContentController: WKUserContentController { - public let newTabPageUserScriptProvider: NewTabPageUserScriptProvider + private let newTabPageUserScriptProvider: NewTabPageUserScriptProvider @MainActor public init(newTabPageUserScript: NewTabPageUserScript) { @@ -50,18 +50,18 @@ public final class NewTabPageUserContentController: WKUserContentController { } @MainActor -public final class NewTabPageUserScriptProvider: UserScriptsProvider { - public lazy var userScripts: [UserScript] = [specialPagesUserScript] +private final class NewTabPageUserScriptProvider: UserScriptsProvider { + lazy var userScripts: [UserScript] = [specialPagesUserScript] - public let specialPagesUserScript: SpecialPagesUserScript + let specialPagesUserScript: SpecialPagesUserScript - public init(newTabPageUserScript: NewTabPageUserScript) { + init(newTabPageUserScript: NewTabPageUserScript) { specialPagesUserScript = SpecialPagesUserScript() specialPagesUserScript.registerSubfeature(delegate: newTabPageUserScript) } @MainActor - public func loadWKUserScripts() async -> [WKUserScript] { + func loadWKUserScripts() async -> [WKUserScript] { return await withTaskGroup(of: WKUserScriptBox.self) { @MainActor group in var wkUserScripts = [WKUserScript]() userScripts.forEach { userScript in diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift index 1663e99604..96a1f8fe07 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageUserScript.swift @@ -60,7 +60,7 @@ public final class NewTabPageUserScript: NSObject, SubfeatureWithExternalMessage messageHandlers[methodName] } - public func pushMessage(named method: String, params: Encodable?, using script: NewTabPageUserScript) { + func pushMessage(named method: MessageName, params: Encodable?, using script: NewTabPageUserScript) { guard let webView = script.webView else { return } @@ -68,33 +68,24 @@ public final class NewTabPageUserScript: NSObject, SubfeatureWithExternalMessage } } -public extension NewTabPageUserScript { +extension NewTabPageUserScript { - public struct WidgetConfig: Codable { - public let animation: Animation? - public let expansion: Expansion + struct WidgetConfig: Codable { + let animation: Animation? + let expansion: Expansion - public init(animation: Animation?, expansion: Expansion) { - self.animation = animation - self.expansion = expansion - } - - public enum Expansion: String, Codable { + enum Expansion: String, Codable { case collapsed, expanded } - public struct Animation: Codable, Equatable { - public let kind: AnimationKind - - public init(kind: AnimationKind) { - self.kind = kind - } + struct Animation: Codable, Equatable { + let kind: AnimationKind - public static let none = Animation(kind: .none) - public static let viewTransitions = Animation(kind: .viewTransitions) - public static let auto = Animation(kind: .auto) + static let none = Animation(kind: .none) + static let viewTransitions = Animation(kind: .viewTransitions) + static let auto = Animation(kind: .auto) - public enum AnimationKind: String, Codable { + enum AnimationKind: String, Codable { case none case viewTransitions = "view-transitions" case auto = "auto-animate" diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index 8f3c2a2e66..de9467d5b2 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -23,8 +23,8 @@ import WebKit public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { - public let model: NewTabPageNextStepsCardsProviding - public let willDisplayCardsPublisher: AnyPublisher<[CardID], Never> + let model: NewTabPageNextStepsCardsProviding + let willDisplayCardsPublisher: AnyPublisher<[CardID], Never> public weak var userScriptsSource: NewTabPageUserScriptsSource? private let willDisplayCardsSubject = PassthroughSubject<[CardID], Never>() @@ -96,7 +96,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { .store(in: &cancellables) } - public enum MessageName: String, CaseIterable { + enum MessageName: String, CaseIterable { case action = "nextSteps_action" case dismiss = "nextSteps_dismiss" case getConfig = "nextSteps_getConfig" @@ -117,7 +117,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - public func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let card: Card = DecodableHelper.decode(from: params) else { return nil } @@ -126,7 +126,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - public func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let card: Card = DecodableHelper.decode(from: params) else { return nil } @@ -134,7 +134,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { return nil } - public func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = model.isViewExpanded ? .expanded : .collapsed getConfigSubject.send(model.isViewExpanded) @@ -142,7 +142,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - public func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { return nil } @@ -151,7 +151,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } @MainActor - public func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let cardIDs = model.cards let cards = cardIDs.map(Card.init(id:)) @@ -178,9 +178,9 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { } } -public extension NewTabPageNextStepsCardsClient { +extension NewTabPageNextStepsCardsClient { - enum CardID: String, Codable { + public enum CardID: String, Codable { case bringStuff case defaultApp case emailProtection @@ -189,18 +189,10 @@ public extension NewTabPageNextStepsCardsClient { } struct Card: Codable, Equatable { - public let id: CardID - - public init(id: CardID) { - self.id = id - } + let id: CardID } struct NextStepsData: Codable, Equatable { public let content: [Card]? - - public init(content: [Card]?) { - self.content = content - } } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift index 7c2f859d21..c7a906d922 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift @@ -28,7 +28,7 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { private let model: NewTabPagePrivacyStatsModel private var cancellables: Set = [] - public enum MessageName: String, CaseIterable { + enum MessageName: String, CaseIterable { case getConfig = "stats_getConfig" case getData = "stats_getData" case onConfigUpdate = "stats_onConfigUpdate" @@ -64,7 +64,7 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { ]) } - public func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func getConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { let expansion: NewTabPageUserScript.WidgetConfig.Expansion = model.isViewExpanded ? .expanded : .collapsed return NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: expansion) } @@ -77,7 +77,7 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { } @MainActor - public func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func setConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let config: NewTabPageUserScript.WidgetConfig = DecodableHelper.decode(from: params) else { return nil } @@ -91,37 +91,27 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { } @MainActor - public func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { return await model.calculatePrivacyStats() } } -public extension NewTabPagePrivacyStatsClient { +extension NewTabPagePrivacyStatsClient { struct PrivacyStatsData: Encodable, Equatable { - public let totalCount: Int64 - public let trackerCompanies: [TrackerCompany] + let totalCount: Int64 + let trackerCompanies: [TrackerCompany] - public init(totalCount: Int64, trackerCompanies: [TrackerCompany]) { - self.totalCount = totalCount - self.trackerCompanies = trackerCompanies - } - - public static func == (lhs: PrivacyStatsData, rhs: PrivacyStatsData) -> Bool { + static func == (lhs: PrivacyStatsData, rhs: PrivacyStatsData) -> Bool { lhs.totalCount == rhs.totalCount && Set(lhs.trackerCompanies) == Set(rhs.trackerCompanies) } } struct TrackerCompany: Encodable, Equatable, Hashable { - public let count: Int64 - public let displayName: String - - public init(count: Int64, displayName: String) { - self.count = count - self.displayName = displayName - } + let count: Int64 + let displayName: String - public static func otherCompanies(count: Int64) -> TrackerCompany { + static func otherCompanies(count: Int64) -> TrackerCompany { TrackerCompany(count: count, displayName: "__other__") } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift index e5ba46311b..06101bfd1c 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift @@ -26,19 +26,19 @@ public protocol NewTabPagePrivacyStatsSettingsPersistor: AnyObject { var isViewExpanded: Bool { get set } } -public final class UserDefaultsNewTabPagePrivacyStatsSettingsPersistor: NewTabPagePrivacyStatsSettingsPersistor { +final class UserDefaultsNewTabPagePrivacyStatsSettingsPersistor: NewTabPagePrivacyStatsSettingsPersistor { enum Keys { static let isViewExpanded = "new-tab-page.privacy-stats.is-view-expanded" } private let keyValueStore: KeyValueStoring - public init(_ keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacySetting: @autoclosure () -> Bool?) { + init(_ keyValueStore: KeyValueStoring = UserDefaults.standard, getLegacySetting: @autoclosure () -> Bool?) { self.keyValueStore = keyValueStore migrateFromLegacyHomePageSettings(using: getLegacySetting) } - public var isViewExpanded: Bool { + var isViewExpanded: Bool { get { return keyValueStore.object(forKey: Keys.isViewExpanded) as? Bool ?? false } set { keyValueStore.set(newValue, forKey: Keys.isViewExpanded) } } @@ -53,10 +53,10 @@ public final class UserDefaultsNewTabPagePrivacyStatsSettingsPersistor: NewTabPa public final class NewTabPagePrivacyStatsModel { - public let privacyStats: PrivacyStatsCollecting - public let statsUpdatePublisher: AnyPublisher + let privacyStats: PrivacyStatsCollecting + let statsUpdatePublisher: AnyPublisher - @Published public var isViewExpanded: Bool { + @Published var isViewExpanded: Bool { didSet { settingsPersistor.isViewExpanded = self.isViewExpanded } @@ -82,7 +82,7 @@ public final class NewTabPagePrivacyStatsModel { ) } - public init( + init( privacyStats: PrivacyStatsCollecting, trackerDataProvider: PrivacyStatsTrackerDataProviding, settingsPersistor: NewTabPagePrivacyStatsSettingsPersistor @@ -111,7 +111,7 @@ public final class NewTabPagePrivacyStatsModel { refreshTopCompanies() } - public func calculatePrivacyStats() async -> NewTabPagePrivacyStatsClient.PrivacyStatsData { + func calculatePrivacyStats() async -> NewTabPagePrivacyStatsClient.PrivacyStatsData { let stats = await privacyStats.fetchPrivacyStats() var totalCount: Int64 = 0 diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift new file mode 100644 index 0000000000..8035ae7d52 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageActiveRemoteMessageProviding.swift @@ -0,0 +1,29 @@ +// +// NewTabPageActiveRemoteMessageProviding.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 RemoteMessaging + +public protocol NewTabPageActiveRemoteMessageProviding { + var remoteMessage: RemoteMessageModel? { get set } + var remoteMessagePublisher: AnyPublisher { get } + + func isMessageSupported(_ message: RemoteMessageModel) -> Bool + + func handleAction(_ action: RemoteAction?, andDismissUsing button: RemoteMessageButton) async +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift index 9132783890..bb8d38a9ac 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift @@ -22,22 +22,13 @@ import RemoteMessaging import UserScript import WebKit -public protocol NewTabPageActiveRemoteMessageProviding { - var remoteMessage: RemoteMessageModel? { get set } - var remoteMessagePublisher: AnyPublisher { get } - - func isMessageSupported(_ message: RemoteMessageModel) -> Bool - - func handleAction(_ action: RemoteAction?, andDismissUsing button: RemoteMessageButton) async -} - public enum RemoteMessageButton: Equatable { case close, action, primaryAction, secondaryAction } public final class NewTabPageRMFClient: NewTabPageScriptClient { - public let remoteMessageProvider: NewTabPageActiveRemoteMessageProviding + let remoteMessageProvider: NewTabPageActiveRemoteMessageProviding public weak var userScriptsSource: NewTabPageUserScriptsSource? private var cancellables = Set() @@ -52,7 +43,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { .store(in: &cancellables) } - public enum MessageName: String, CaseIterable { + enum MessageName: String, CaseIterable { case rmfGetData = "rmf_getData" case rmfOnDataUpdate = "rmf_onDataUpdate" case rmfDismiss = "rmf_dismiss" @@ -135,24 +126,24 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { } } -public extension NewTabPageUserScript { +extension NewTabPageUserScript { struct RemoteMessageParams: Codable { - public let id: String + let id: String } struct RMFData: Encodable { - public let content: RMFMessage? + let content: RMFMessage? } enum RMFMessage: Encodable, Equatable { case small(SmallMessage), medium(MediumMessage), bigSingleAction(BigSingleActionMessage), bigTwoAction(BigTwoActionMessage) - public func encode(to encoder: any Encoder) throws { + func encode(to encoder: any Encoder) throws { try message.encode(to: encoder) } - public var message: Encodable { + var message: Encodable { switch self { case .small(let message): return message @@ -165,7 +156,7 @@ public extension NewTabPageUserScript { } } - public init?(_ remoteMessageModel: RemoteMessageModel) { + init?(_ remoteMessageModel: RemoteMessageModel) { guard let modelType = remoteMessageModel.content else { return nil } @@ -190,41 +181,41 @@ public extension NewTabPageUserScript { } struct SmallMessage: Encodable, Equatable { - public let messageType = "small" + let messageType = "small" - public let id: String - public let titleText: String - public let descriptionText: String + let id: String + let titleText: String + let descriptionText: String } struct MediumMessage: Encodable, Equatable { - public let messageType = "medium" + let messageType = "medium" - public let id: String - public let titleText: String - public let descriptionText: String - public let icon: RMFIcon + let id: String + let titleText: String + let descriptionText: String + let icon: RMFIcon } struct BigSingleActionMessage: Encodable, Equatable { - public let messageType = "big_single_action" + let messageType = "big_single_action" - public let id: String - public let titleText: String - public let descriptionText: String - public let icon: RMFIcon - public let primaryActionText: String + let id: String + let titleText: String + let descriptionText: String + let icon: RMFIcon + let primaryActionText: String } struct BigTwoActionMessage: Encodable, Equatable { - public let messageType = "big_two_action" + let messageType = "big_two_action" - public let id: String - public let titleText: String - public let descriptionText: String - public let icon: RMFIcon - public let primaryActionText: String - public let secondaryActionText: String + let id: String + let titleText: String + let descriptionText: String + let icon: RMFIcon + let primaryActionText: String + let secondaryActionText: String } enum RMFIcon: String, Encodable { @@ -234,7 +225,7 @@ public extension NewTabPageUserScript { case appUpdate = "AppUpdate" case privacyPro = "PrivacyPro" - public init(_ placeholder: RemotePlaceholder) { + init(_ placeholder: RemotePlaceholder) { switch placeholder { case .announce: self = .announce diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift new file mode 100644 index 0000000000..4cc61f4c50 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift @@ -0,0 +1,52 @@ +// +// CapturingNewTabPageNextStepsCardsProvider.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 XCTest +@testable import NewTabPage + +final class CapturingNewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding { + + @Published var isViewExpanded: Bool = false + var isViewExpandedPublisher: AnyPublisher { + $isViewExpanded.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + @Published var cards: [NewTabPageNextStepsCardsClient.CardID] = [] + var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { + $cards.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) { + handleActionCalls.append(card) + } + + func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { + dismissCalls.append(card) + } + + func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { + willDisplayCardsCalls.append(cards) + willDisplayCardsImpl?(cards) + } + + var handleActionCalls: [NewTabPageNextStepsCardsClient.CardID] = [] + var dismissCalls: [NewTabPageNextStepsCardsClient.CardID] = [] + var willDisplayCardsCalls: [[NewTabPageNextStepsCardsClient.CardID]] = [] + var willDisplayCardsImpl: (([NewTabPageNextStepsCardsClient.CardID]) -> Void)? +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift index e23edb530c..2d8160d72d 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift @@ -21,37 +21,6 @@ import TestUtils import XCTest @testable import NewTabPage -final class CapturingNewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding { - - @Published var isViewExpanded: Bool = false - var isViewExpandedPublisher: AnyPublisher { - $isViewExpanded.dropFirst().removeDuplicates().eraseToAnyPublisher() - } - - @Published var cards: [NewTabPageNextStepsCardsClient.CardID] = [] - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { - $cards.dropFirst().removeDuplicates().eraseToAnyPublisher() - } - - func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) { - handleActionCalls.append(card) - } - - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { - dismissCalls.append(card) - } - - func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { - willDisplayCardsCalls.append(cards) - willDisplayCardsImpl?(cards) - } - - var handleActionCalls: [NewTabPageNextStepsCardsClient.CardID] = [] - var dismissCalls: [NewTabPageNextStepsCardsClient.CardID] = [] - var willDisplayCardsCalls: [[NewTabPageNextStepsCardsClient.CardID]] = [] - var willDisplayCardsImpl: (([NewTabPageNextStepsCardsClient.CardID]) -> Void)? -} - final class NewTabPageNextStepsCardsClientTests: XCTestCase { var client: NewTabPageNextStepsCardsClient! var model: CapturingNewTabPageNextStepsCardsProvider! @@ -143,38 +112,38 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { func testThatWillDisplayCardsPublisherIsSentAfterGetDataAndGetConfigAreCalled() async throws { model.cards = [.addAppToDockMac, .duckplayer] - _ = try await client.getData(params: [], original: .init()) - _ = try await client.getConfig(params: [], original: .init()) + try await handleMessageIgnoringResponse(named: .getData) + try await handleMessageIgnoringResponse(named: .getConfig) XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) } func testThatWillDisplayCardsPublisherIsNotSentBeforeGetConfigIsCalled() async throws { model.cards = [.addAppToDockMac, .duckplayer] - _ = try await client.getData(params: [], original: .init()) - _ = try await client.getData(params: [], original: .init()) - _ = try await client.getData(params: [], original: .init()) - _ = try await client.getData(params: [], original: .init()) - _ = try await client.getData(params: [], original: .init()) + try await handleMessageIgnoringResponse(named: .getData) + try await handleMessageIgnoringResponse(named: .getData) + try await handleMessageIgnoringResponse(named: .getData) + try await handleMessageIgnoringResponse(named: .getData) + try await handleMessageIgnoringResponse(named: .getData) XCTAssertEqual(model.willDisplayCardsCalls, []) - _ = try await client.getConfig(params: [], original: .init()) + try await handleMessageIgnoringResponse(named: .getConfig) XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) } func testThatWillDisplayCardsPublisherIsNotSentBeforeGetDataIsCalled() async throws { model.cards = [.addAppToDockMac, .duckplayer] - _ = try await client.getConfig(params: [], original: .init()) - _ = try await client.getConfig(params: [], original: .init()) - _ = try await client.getConfig(params: [], original: .init()) - _ = try await client.getConfig(params: [], original: .init()) - _ = try await client.getConfig(params: [], original: .init()) + try await handleMessageIgnoringResponse(named: .getConfig) + try await handleMessageIgnoringResponse(named: .getConfig) + try await handleMessageIgnoringResponse(named: .getConfig) + try await handleMessageIgnoringResponse(named: .getConfig) + try await handleMessageIgnoringResponse(named: .getConfig) XCTAssertEqual(model.willDisplayCardsCalls, []) - _ = try await client.getData(params: [], original: .init()) + try await handleMessageIgnoringResponse(named: .getData) XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) } @@ -275,8 +244,8 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { // MARK: - Helper functions func triggerInitialCardsEventAndResetMockState() async throws { - _ = try await client.getConfig(params: [], original: .init()) - _ = try await client.getData(params: [], original: .init()) + try await handleMessageIgnoringResponse(named: .getConfig) + try await handleMessageIgnoringResponse(named: .getData) model.willDisplayCardsCalls = [] } @@ -286,6 +255,11 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { return try XCTUnwrap(response as? Response, file: file, line: line) } + func handleMessageIgnoringResponse(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + } + func handleMessageExpectingNilResponse(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) From 84374132b57a9c845d6d08f21e47640715f4cca7 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 20:12:11 +0100 Subject: [PATCH 25/62] Add initial version of the customizer --- DuckDuckGo.xcodeproj/project.pbxproj | 8 + .../xcshareddata/swiftpm/Package.resolved | 377 +++++++++--------- .../HomePageSettingsModel+NewTabPage.swift | 102 +++++ .../NewTabPageActionsManagerExtension.swift | 4 +- .../Tab/Navigation/DuckURLSchemeHandler.swift | 76 +++- .../AppKitExtensions/NSImageExtension.swift | 9 + .../NewTabPageCustomBackgroundClient.swift | 96 +++++ .../NewTabPageConfigurationClient.swift | 74 +++- 8 files changed, 549 insertions(+), 197 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1f7ce6a92e..981c3daa67 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1233,6 +1233,8 @@ 37BF3F14286D8A6500BD9014 /* PinnedTabsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F13286D8A6500BD9014 /* PinnedTabsManager.swift */; }; 37BF3F21286F0A7A00BD9014 /* PinnedTabsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */; }; 37BF3F22286F0A7A00BD9014 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; + 37C33BD12D02154300252656 /* HomePageSettingsModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C33BD02D02153E00252656 /* HomePageSettingsModel+NewTabPage.swift */; }; + 37C33BD22D02154300252656 /* HomePageSettingsModel+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C33BD02D02153E00252656 /* HomePageSettingsModel+NewTabPage.swift */; }; 37C9F78C2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; 37C9F78D2CF1C776004D73A1 /* PrivacyStatsTabExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */; }; 37CBCA9A2A8966E60050218F /* SyncSettingsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */; }; @@ -3721,6 +3723,8 @@ 37BF3F13286D8A6500BD9014 /* PinnedTabsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTabsManager.swift; sourceTree = ""; }; 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; + 37C33BCF2D01E92B00252656 /* content-scope-scripts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "content-scope-scripts"; path = "../content-scope-scripts"; sourceTree = SOURCE_ROOT; }; + 37C33BD02D02153E00252656 /* HomePageSettingsModel+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomePageSettingsModel+NewTabPage.swift"; sourceTree = ""; }; 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsTabExtension.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; @@ -5811,6 +5815,7 @@ 376788092CECCAD900F59D83 /* NewTabPage */ = { isa = PBXGroup; children = ( + 37C33BD02D02153E00252656 /* HomePageSettingsModel+NewTabPage.swift */, 374EFDF22D01C99700B30939 /* ActiveRemoteMessageModel+NewTabPage.swift */, 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */, 372D15E82D00F18E00A11576 /* ContinueSetUpModel+NewTabPage.swift */, @@ -7836,6 +7841,7 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( + 37C33BCF2D01E92B00252656 /* content-scope-scripts */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, @@ -11980,6 +11986,7 @@ B6685E4329A61C470043D2EE /* DownloadsTabExtension.swift in Sources */, 1DA84D332C119AE70011C80F /* UpdateMenuItemFactory.swift in Sources */, 4BCBE4562BA7E16900FC75A1 /* DataBrokerProtectionSubscriptionEventHandler.swift in Sources */, + 37C33BD12D02154300252656 /* HomePageSettingsModel+NewTabPage.swift in Sources */, 3706FC44293F65D500E42796 /* BookmarkList.swift in Sources */, 3706FC45293F65D500E42796 /* BookmarkTableRowView.swift in Sources */, 7BEC20462B0F505F00243D3E /* AddBookmarkFolderPopoverView.swift in Sources */, @@ -12996,6 +13003,7 @@ 1DDF076428F815AD00EDFBE3 /* BWManager.swift in Sources */, 9833912F27AAA3CE00DAF119 /* AppTrackerDataSetProvider.swift in Sources */, B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */, + 37C33BD22D02154300252656 /* HomePageSettingsModel+NewTabPage.swift in Sources */, 7BDBAD182CBFF633000379B7 /* TipKitAppEventHandling.swift in Sources */, 7BDBAD192CBFF633000379B7 /* TipKitController+ConvenienceInitializers.swift in Sources */, 7BDBAD1A2CBFF633000379B7 /* TipKitDebugOptionsUIActionHandling.swift in Sources */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5e825895d4..27bb50441a 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,194 +1,187 @@ { - "pins" : [ - { - "identity" : "apple-toolbox", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/apple-toolbox.git", - "state" : { - "revision" : "0c13c5f056805f2d403618ccc3bfb833c303c68d", - "version" : "3.1.2" - } - }, - { - "identity" : "barebonesbrowser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/BareBonesBrowser.git", - "state" : { - "revision" : "31e5bfedc3c2ca005640c4bf2b6959d69b0e18b9", - "version" : "0.1.0" - } - }, - { - "identity" : "bloom_cpp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/bloom_cpp.git", - "state" : { - "revision" : "8076199456290b61b4544bf2f4caf296759906a0", - "version" : "3.0.0" - } - }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/BrowserServicesKit", - "state" : { - "revision" : "f0755fbb3309c93c8490dc8bbdfb7e2e7613bef6", - "version" : "217.0.2" - } - }, - { - "identity" : "content-scope-scripts", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/content-scope-scripts", - "state" : { - "revision" : "c4bb146afdf0c7a93fb9a7d95b1cb255708a470d", - "version" : "6.41.0" - } - }, - { - "identity" : "duckduckgo-autofill", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", - "state" : { - "revision" : "c992041d16ec10d790e6204dce9abf9966d1363c", - "version" : "15.1.0" - } - }, - { - "identity" : "grdb.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/GRDB.swift.git", - "state" : { - "revision" : "5b2f6a81099d26ae0f9e38788f51490cd6a4b202", - "version" : "2.4.2" - } - }, - { - "identity" : "gzipswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/1024jp/GzipSwift.git", - "state" : { - "revision" : "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", - "version" : "6.0.1" - } - }, - { - "identity" : "lottie-spm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-spm", - "state" : { - "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", - "version" : "4.4.3" - } - }, - { - "identity" : "ohhttpstubs", - "kind" : "remoteSourceControl", - "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", - "state" : { - "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version" : "9.1.0" - } - }, - { - "identity" : "openssl-xcframework", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/OpenSSL-XCFramework", - "state" : { - "revision" : "71d303cbfa150e1fac99ffc7b4f67aad9c7a5002", - "version" : "3.1.5004" - } - }, - { - "identity" : "privacy-dashboard", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/privacy-dashboard", - "state" : { - "revision" : "49db79829dcb166b3524afdbc1c680890452ce1c", - "version" : "7.2.1" - } - }, - { - "identity" : "punycodeswift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/gumob/PunycodeSwift.git", - "state" : { - "revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5", - "version" : "3.0.0" - } - }, - { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle.git", - "state" : { - "revision" : "b456fd404954a9e13f55aa0c88cd5a40b8399638", - "version" : "2.6.3" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser.git", - "state" : { - "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version" : "1.4.0" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", - "state" : { - "revision" : "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd", - "version" : "1.15.4" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - }, - { - "identity" : "swifter", - "kind" : "remoteSourceControl", - "location" : "https://github.com/httpswift/swifter.git", - "state" : { - "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version" : "1.5.0" - } - }, - { - "identity" : "sync_crypto", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/sync_crypto", - "state" : { - "revision" : "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", - "version" : "0.3.0" - } - }, - { - "identity" : "trackerradarkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", - "state" : { - "revision" : "5de0a610a7927b638a5fd463a53032c9934a2c3b", - "version" : "3.0.0" - } - }, - { - "identity" : "wireguard-apple", - "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/wireguard-apple", - "state" : { - "revision" : "13fd026384b1af11048451061cc1b21434990668", - "version" : "1.1.3" - } - } - ], - "version" : 2 + "object": { + "pins": [ + { + "package": "AppleToolbox", + "repositoryURL": "https://github.com/duckduckgo/apple-toolbox.git", + "state": { + "branch": null, + "revision": "0c13c5f056805f2d403618ccc3bfb833c303c68d", + "version": "3.1.2" + } + }, + { + "package": "BareBonesBrowserKit", + "repositoryURL": "https://github.com/duckduckgo/BareBonesBrowser.git", + "state": { + "branch": null, + "revision": "31e5bfedc3c2ca005640c4bf2b6959d69b0e18b9", + "version": "0.1.0" + } + }, + { + "package": "BloomFilter", + "repositoryURL": "https://github.com/duckduckgo/bloom_cpp.git", + "state": { + "branch": null, + "revision": "8076199456290b61b4544bf2f4caf296759906a0", + "version": "3.0.0" + } + }, + { + "package": "BrowserServicesKit", + "repositoryURL": "https://github.com/duckduckgo/BrowserServicesKit", + "state": { + "branch": null, + "revision": "f0755fbb3309c93c8490dc8bbdfb7e2e7613bef6", + "version": "217.0.2" + } + }, + { + "package": "Autofill", + "repositoryURL": "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state": { + "branch": null, + "revision": "c992041d16ec10d790e6204dce9abf9966d1363c", + "version": "15.1.0" + } + }, + { + "package": "GRDB", + "repositoryURL": "https://github.com/duckduckgo/GRDB.swift.git", + "state": { + "branch": null, + "revision": "5b2f6a81099d26ae0f9e38788f51490cd6a4b202", + "version": "2.4.2" + } + }, + { + "package": "Gzip", + "repositoryURL": "https://github.com/1024jp/GzipSwift.git", + "state": { + "branch": null, + "revision": "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", + "version": "6.0.1" + } + }, + { + "package": "Lottie", + "repositoryURL": "https://github.com/airbnb/lottie-spm", + "state": { + "branch": null, + "revision": "1d29eccc24cc8b75bff9f6804155112c0ffc9605", + "version": "4.4.3" + } + }, + { + "package": "OHHTTPStubs", + "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", + "state": { + "branch": null, + "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version": "9.1.0" + } + }, + { + "package": "OpenSSL", + "repositoryURL": "https://github.com/duckduckgo/OpenSSL-XCFramework", + "state": { + "branch": null, + "revision": "71d303cbfa150e1fac99ffc7b4f67aad9c7a5002", + "version": "3.1.5004" + } + }, + { + "package": "PrivacyDashboardResources", + "repositoryURL": "https://github.com/duckduckgo/privacy-dashboard", + "state": { + "branch": null, + "revision": "49db79829dcb166b3524afdbc1c680890452ce1c", + "version": "7.2.1" + } + }, + { + "package": "Punycode", + "repositoryURL": "https://github.com/gumob/PunycodeSwift.git", + "state": { + "branch": null, + "revision": "30a462bdb4398ea835a3585472229e0d74b36ba5", + "version": "3.0.0" + } + }, + { + "package": "Sparkle", + "repositoryURL": "https://github.com/sparkle-project/Sparkle.git", + "state": { + "branch": null, + "revision": "b456fd404954a9e13f55aa0c88cd5a40b8399638", + "version": "2.6.3" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser.git", + "state": { + "branch": null, + "revision": "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version": "1.4.0" + } + }, + { + "package": "swift-snapshot-testing", + "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing", + "state": { + "branch": null, + "revision": "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd", + "version": "1.15.4" + } + }, + { + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax", + "state": { + "branch": null, + "revision": "64889f0c732f210a935a0ad7cda38f77f876262d", + "version": "509.1.1" + } + }, + { + "package": "Swifter", + "repositoryURL": "https://github.com/httpswift/swifter.git", + "state": { + "branch": null, + "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", + "version": "1.5.0" + } + }, + { + "package": "DDGSyncCrypto", + "repositoryURL": "https://github.com/duckduckgo/sync_crypto", + "state": { + "branch": null, + "revision": "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", + "version": "0.3.0" + } + }, + { + "package": "TrackerRadarKit", + "repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit", + "state": { + "branch": null, + "revision": "5de0a610a7927b638a5fd463a53032c9934a2c3b", + "version": "3.0.0" + } + }, + { + "package": "WireGuardKit", + "repositoryURL": "https://github.com/duckduckgo/wireguard-apple", + "state": { + "branch": null, + "revision": "13fd026384b1af11048451061cc1b21434990668", + "version": "1.1.3" + } + } + ] + }, + "version": 1 } diff --git a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift new file mode 100644 index 0000000000..3cbf4d43a7 --- /dev/null +++ b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift @@ -0,0 +1,102 @@ +// +// HomePageSettingsModel+NewTabPage.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 NewTabPage +import SwiftUI + +final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding { + let homePageSettingsModel: HomePage.Models.SettingsModel + let appearancePreferences: AppearancePreferences + + init(homePageSettingsModel: HomePage.Models.SettingsModel, appearancePreferences: AppearancePreferences = .shared) { + self.homePageSettingsModel = homePageSettingsModel + self.appearancePreferences = appearancePreferences + } + + var customizerData: NewTabPageUserScript.CustomizerData { + .init( + background: .init(homePageSettingsModel.customBackground), + theme: .init(appearancePreferences.currentThemeName), + userImages: homePageSettingsModel.availableUserBackgroundImages.map(NewTabPageUserScript.UserImage.init) + ) + } +} + +extension NewTabPageUserScript.Background { + init(_ customBackground: CustomBackground?) { + switch customBackground { + case .gradient(let gradient): + self = .gradient(gradient.rawValue) + case .solidColor(let solidColor): + if let predefinedColorName = solidColor.predefinedColorName { + self = .solidColor(predefinedColorName) + } else { + self = .hexColor(solidColor.description) + } + case .userImage(let userBackgroundImage): + self = .userImage(.init(userBackgroundImage)) + case .none: + self = .default + } + } +} + +extension NewTabPageUserScript.UserImage { + init(_ userBackgroundImage: UserBackgroundImage) { + self.init( + colorScheme: .init(userBackgroundImage.colorScheme), + id: userBackgroundImage.id, + src: "/background/images/\(userBackgroundImage.fileName)", + thumb: "/background/thumbnails/\(userBackgroundImage.fileName)" + ) + } +} + +extension NewTabPageUserScript.Theme { + init(_ colorScheme: ColorScheme) { + switch colorScheme { + case .dark: + self = .dark + case .light: + self = .light + @unknown default: + self = .light + } + } + + init?(_ themeName: ThemeName) { + switch themeName { + case .light: + self = .light + case .dark: + self = .dark + case .systemDefault: + return nil + } + } +} + +extension URL { + static func duckUserBackgroundImage(for fileName: String) -> URL? { + return URL(string: "duck://user-background-image/\(fileName)") + } + + static func duckUserBackgroundImageThumbnail(for fileName: String) -> URL? { + return URL(string: "duck://user-background-image/thumbnails/\(fileName)") + } +} diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 4d92b5ecc0..32f305a916 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -40,8 +40,10 @@ extension NewTabPageActionsManager { getLegacyIsViewExpandedSetting: UserDefaultsWrapper(key: .homePageShowAllFavorites, defaultValue: false).wrappedValue ) + let customizationProvider = NewTabPageCustomizationProvider(homePageSettingsModel: NSApp.delegateTyped.homePageSettingsModel) + self.init(scriptClients: [ - NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences), + NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences, customBackgroundProvider: customizationProvider), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: Int(Favicon.SizeCategory.medium.rawValue)), diff --git a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift index 0f98447a2b..57dc34c3d9 100644 --- a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift @@ -28,15 +28,18 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { let featureFlagger: FeatureFlagger let faviconManager: FaviconManagement let isNTPSpecialPageSupported: Bool + let userBackgroundImagesManager: UserBackgroundImagesManaging? init( featureFlagger: FeatureFlagger, faviconManager: FaviconManagement = FaviconManager.shared, - isNTPSpecialPageSupported: Bool = false + isNTPSpecialPageSupported: Bool = false, + userBackgroundImagesManager: UserBackgroundImagesManaging? = NSApp.delegateTyped.homePageSettingsModel.customImagesManager ) { self.featureFlagger = featureFlagger self.faviconManager = faviconManager self.isNTPSpecialPageSupported = isNTPSpecialPageSupported + self.userBackgroundImagesManager = userBackgroundImagesManager } func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { @@ -54,9 +57,14 @@ final class DuckURLSchemeHandler: NSObject, WKURLSchemeHandler { case .error: handleErrorPage(urlSchemeTask: urlSchemeTask) case .newTab where isNTPSpecialPageSupported && featureFlagger.isFeatureOn(.htmlNewTabPage): - if requestURL.type == .favicon { + switch requestURL.type { + case .favicon: handleFavicon(urlSchemeTask: urlSchemeTask) - } else { + case .customBackgroundImage: + handleCustomBackgroundImage(urlSchemeTask: urlSchemeTask) + case .customBackgroundImageThumbnail: + handleCustomBackgroundImage(urlSchemeTask: urlSchemeTask, isThumbnail: true) + default: handleSpecialPages(urlSchemeTask: urlSchemeTask) } default: @@ -176,6 +184,52 @@ private extension DuckURLSchemeHandler { } } +// MARK: - Custom Background Images + +private extension DuckURLSchemeHandler { + /** + * This handler supports special Duck favicon URLs and uses `FaviconManager` + * to return a favicon in response, based on the actual favicon URL that's + * encoded in the URL path. + * + * If favicon is not found, an `HTTP 404` response is returned. + */ + func handleCustomBackgroundImage(urlSchemeTask: WKURLSchemeTask, isThumbnail: Bool = false) { + guard let requestURL = urlSchemeTask.request.url else { + assertionFailure("No URL for Favicon scheme handler") + return + } + + /** + * Favicon URL has the format of `duck://favicon/`. + * Calling `requestURL.path` drops leading `duck://favicon` and automatically + * handles percent-encoding. We only need to drop the leading forward slash to get the favicon URL. + */ + let fileName = requestURL.lastPathComponent + + guard let (response, data) = response(for: requestURL, withFileName: fileName, isThumbnail: isThumbnail) else { return } + urlSchemeTask.didReceive(response) + urlSchemeTask.didReceive(data) + urlSchemeTask.didFinish() + } + + func response(for requestURL: URL, withFileName fileName: String, isThumbnail: Bool) -> (URLResponse, Data)? { + guard let userBackgroundImagesManager, + let userBackgroundImage = userBackgroundImagesManager.availableImages.first(where: { $0.fileName == fileName }), + let image = isThumbnail ? userBackgroundImagesManager.thumbnailImage(for: userBackgroundImage) : userBackgroundImagesManager.image(for: userBackgroundImage), + let imageJPEGData = image.jpegData + else { + guard let response = HTTPURLResponse(url: requestURL, statusCode: 404, httpVersion: "HTTP/1.1", headerFields: nil) else { + return nil + } + return (response, Data()) + } + + let response = URLResponse(url: requestURL, mimeType: "image/jpeg", expectedContentLength: imageJPEGData.count, textEncodingName: nil) + return (response, imageJPEGData) + } +} + // MARK: - Onboarding & Release Notes private extension DuckURLSchemeHandler { func handleSpecialPages(urlSchemeTask: WKURLSchemeTask) { @@ -276,6 +330,8 @@ private extension URL { enum URLType { case newTab case favicon + case customBackgroundImage + case customBackgroundImageThumbnail case onboarding case duckPlayer case releaseNotes @@ -292,6 +348,12 @@ private extension URL { } else if self.isReleaseNotes { return .releaseNotes } else if self.isNewTabPage { + if self.isCustomBackgroundImage { + return .customBackgroundImage + } + if self.isCustomBackgroundImageThumbnail { + return .customBackgroundImageThumbnail + } return .newTab } else if self.isFavicon { return .favicon @@ -316,4 +378,12 @@ private extension URL { return isDuckURLScheme && host == "favicon" } + var isCustomBackgroundImage: Bool { + return isNewTabPage && pathComponents.prefix(2) == ["background", "images"] + } + + var isCustomBackgroundImageThumbnail: Bool { + return isNewTabPage && pathComponents.prefix(2) == ["background", "thumbnails"] + } + } diff --git a/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSImageExtension.swift b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSImageExtension.swift index 063f86f2da..dc238b284f 100644 --- a/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSImageExtension.swift +++ b/LocalPackages/AppKitExtensions/Sources/AppKitExtensions/NSImageExtension.swift @@ -29,6 +29,15 @@ extension NSImage { return bitmapImage.representation(using: .png, properties: [:]) } + public var jpegData: Data? { + guard let tiffData = self.tiffRepresentation, + let bitmapImage = NSBitmapImageRep(data: tiffData) else { + return nil + } + + return bitmapImage.representation(using: .jpeg, properties: [:]) + } + /** * This function calculates image brightness using relative luminance formula. * diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift new file mode 100644 index 0000000000..bf2ecf6b2e --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -0,0 +1,96 @@ +// +// NewTabPageCustomBackgroundClient.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 Common +import Combine +import UserScript +import WebKit + +public protocol NewTabPageCustomBackgroundProviding: AnyObject { + var customizerData: NewTabPageUserScript.CustomizerData { get } +} + +public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { + + let model: NewTabPageCustomBackgroundProviding + public weak var userScriptsSource: NewTabPageUserScriptsSource? + + private var cancellables: Set = [] + + public init(model: NewTabPageCustomBackgroundProviding) { + self.model = model + } + + enum MessageName: String, CaseIterable { + case deleteImage = "customizer_deleteImage" + case onBackgroundUpdate = "customizer_onBackgroundUpdate" + case onImagesUpdate = "customizer_onImagesUpdate" + case onThemeUpdate = "customizer_onThemeUpdate" + case setBackground = "customizer_setBackground" + case setTheme = "customizer_setTheme" + case upload = "customizer_upload" + } + + public func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + userScript.registerMessageHandlers([ + MessageName.deleteImage.rawValue: { [weak self] in try await self?.deleteImage(params: $0, original: $1) }, + MessageName.setBackground.rawValue: { [weak self] in try await self?.setBackground(params: $0, original: $1) }, + MessageName.setTheme.rawValue: { [weak self] in try await self?.setTheme(params: $0, original: $1) }, + MessageName.upload.rawValue: { [weak self] in try await self?.upload(params: $0, original: $1) }, + ]) + } + + @MainActor + func deleteImage(params: Any, original: WKScriptMessage) async throws -> Encodable? { + return nil + } + + @MainActor + private func setBackground(params: Any, original: WKScriptMessage) async throws -> Encodable? { + return nil + } + + @MainActor + private func setTheme(params: Any, original: WKScriptMessage) async throws -> Encodable? { + return nil + } + + @MainActor + private func upload(params: Any, original: WKScriptMessage) async throws -> Encodable? { + return nil + } +} + +extension NewTabPageCustomBackgroundClient { + + public enum CardID: String, Codable { + case bringStuff + case defaultApp + case emailProtection + case duckplayer + case addAppToDockMac + } + + struct Card: Codable, Equatable { + let id: CardID + } + + struct NextStepsData: Codable, Equatable { + public let content: [Card]? + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index fa7911b433..7e72e7ae89 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -37,13 +37,16 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { private var cancellables = Set() private let sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding + private let customBackgroundProvider: NewTabPageCustomBackgroundProviding private let contextMenuPresenter: NewTabPageContextMenuPresenting public init( sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding, + customBackgroundProvider: NewTabPageCustomBackgroundProviding, contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter() ) { self.sectionsVisibilityProvider = sectionsVisibilityProvider + self.customBackgroundProvider = customBackgroundProvider self.contextMenuPresenter = contextMenuPresenter Publishers.Merge(sectionsVisibilityProvider.isFavoritesVisiblePublisher, sectionsVisibilityProvider.isPrivacyStatsVisiblePublisher) @@ -132,6 +135,8 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { #else let env = "production" #endif + + let customizerData = customBackgroundProvider.customizerData return NewTabPageUserScript.NewTabPageConfiguration( widgets: [ .init(id: .rmf), @@ -145,7 +150,9 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { ], env: env, locale: Bundle.main.preferredLocalizations.first ?? "en", - platform: .init(name: "macos") + platform: .init(name: "macos"), + settings: .init(customizerDrawer: .enabled), + customizer: customizerData ) } @@ -197,6 +204,8 @@ extension NewTabPageUserScript { var env: String var locale: String var platform: Platform + var settings: Settings + var customizer: CustomizerData struct Widget: Encodable, Equatable { public var id: WidgetId @@ -224,5 +233,68 @@ extension NewTabPageUserScript { struct Platform: Encodable, Equatable { var name: String } + + struct Settings: Encodable, Equatable { + let customizerDrawer: BooleanSetting + + enum BooleanSetting: String, Encodable { + case enabled, disabled + + var isEnabled: Bool { + self == .enabled + } + } + } + } + + public struct CustomizerData: Encodable, Equatable { + public let background: Background + public let theme: Theme? + public let userImages: [UserImage] + + public init(background: Background, theme: Theme?, userImages: [UserImage]) { + self.background = background + self.theme = theme + self.userImages = userImages + } + + enum CodingKeys: CodingKey { + case background + case theme + case userImages + } + + public func encode(to encoder: any Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: NewTabPageUserScript.CustomizerData.CodingKeys.self) + try container.encode(self.background, forKey: NewTabPageUserScript.CustomizerData.CodingKeys.background) + try container.encode(self.theme?.rawValue ?? "system", forKey: NewTabPageUserScript.CustomizerData.CodingKeys.theme) + try container.encode(self.userImages, forKey: NewTabPageUserScript.CustomizerData.CodingKeys.userImages) + } + } + + public enum Theme: String, Encodable { + case dark, light + } + + public enum Background: Encodable, Equatable { + case `default` + case solidColor(String) + case hexColor(String) + case gradient(String) + case userImage(UserImage) + } + + public struct UserImage: Encodable, Equatable { + public let colorScheme: Theme + public let id: String + public let src: String + public let thumb: String + + public init(colorScheme: Theme, id: String, src: String, thumb: String) { + self.colorScheme = colorScheme + self.id = id + self.src = src + self.thumb = thumb + } } } From fc4db81a1fb8d4f5bfd918895e28899c43d9c91b Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 5 Dec 2024 21:20:26 +0100 Subject: [PATCH 26/62] Fix config schema and image URL handling --- .../Tab/Navigation/DuckURLSchemeHandler.swift | 4 +- .../NewTabPageConfigurationClient.swift | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift index 57dc34c3d9..64b05dc95c 100644 --- a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift @@ -379,11 +379,11 @@ private extension URL { } var isCustomBackgroundImage: Bool { - return isNewTabPage && pathComponents.prefix(2) == ["background", "images"] + return isNewTabPage && pathComponents.prefix(3) == ["/", "background", "images"] } var isCustomBackgroundImageThumbnail: Bool { - return isNewTabPage && pathComponents.prefix(2) == ["background", "thumbnails"] + return isNewTabPage && pathComponents.prefix(3) == ["/", "background", "thumbnails"] } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index 7e72e7ae89..846fcded49 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -137,7 +137,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { #endif let customizerData = customBackgroundProvider.customizerData - return NewTabPageUserScript.NewTabPageConfiguration( + let config = NewTabPageUserScript.NewTabPageConfiguration( widgets: [ .init(id: .rmf), .init(id: .nextSteps), @@ -151,9 +151,10 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { env: env, locale: Bundle.main.preferredLocalizations.first ?? "en", platform: .init(name: "macos"), - settings: .init(customizerDrawer: .enabled), + settings: .init(customizerDrawer: .init(state: .enabled)), customizer: customizerData ) + return config } @MainActor @@ -235,14 +236,18 @@ extension NewTabPageUserScript { } struct Settings: Encodable, Equatable { - let customizerDrawer: BooleanSetting + let customizerDrawer: Setting + } + + struct Setting: Encodable, Equatable { + let state: BooleanSetting + } - enum BooleanSetting: String, Encodable { - case enabled, disabled + enum BooleanSetting: String, Encodable { + case enabled, disabled - var isEnabled: Bool { - self == .enabled - } + var isEnabled: Bool { + self == .enabled } } } @@ -282,6 +287,39 @@ extension NewTabPageUserScript { case hexColor(String) case gradient(String) case userImage(UserImage) + + enum CodingKeys: CodingKey { + case kind + case value + } + + var kind: String { + switch self { + case .default: + return "default" + case .solidColor: + return "solidColor" + case .hexColor: + return "hexColor" + case .gradient: + return "gradient" + case .userImage: + return "userImage" + } + } + + public func encode(to encoder: any Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: NewTabPageUserScript.Background.CodingKeys.self) + try container.encode(kind, forKey: NewTabPageUserScript.Background.CodingKeys.kind) + switch self { + case .default: + break + case .solidColor(let value), .hexColor(let value), .gradient(let value): + try container.encode(value, forKey: NewTabPageUserScript.Background.CodingKeys.value) + case .userImage(let image): + try container.encode(image, forKey: NewTabPageUserScript.Background.CodingKeys.value) + } + } } public struct UserImage: Encodable, Equatable { From 8692de55ddfad158c65f92c59b76eda743f2a1f3 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 6 Dec 2024 18:55:05 +0100 Subject: [PATCH 27/62] Implement setting custom backgrounds --- .../HomePageSettingsModel+NewTabPage.swift | 53 +++++++++++++++++++ .../NewTabPageActionsManagerExtension.swift | 1 + .../NewTabPageCustomBackgroundClient.swift | 24 +++++++++ .../NewTabPageConfigurationClient.swift | 40 ++++++++++---- 4 files changed, 109 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift index 3cbf4d43a7..98a09f3ab3 100644 --- a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import Combine import NewTabPage import SwiftUI @@ -35,6 +36,26 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding userImages: homePageSettingsModel.availableUserBackgroundImages.map(NewTabPageUserScript.UserImage.init) ) } + + var background: NewTabPageUserScript.Background { + get { + .init(homePageSettingsModel.customBackground) + } + set { + homePageSettingsModel.customBackground = .init(newValue) + } + } + + var backgroundPublisher: AnyPublisher { + homePageSettingsModel.$customBackground.dropFirst().removeDuplicates() + .map(NewTabPageUserScript.Background.init) + .eraseToAnyPublisher() + } + + @MainActor + func presentUploadDialog() async{ + await homePageSettingsModel.addNewImage() + } } extension NewTabPageUserScript.Background { @@ -56,6 +77,27 @@ extension NewTabPageUserScript.Background { } } +extension CustomBackground { + init?(_ background: NewTabPageUserScript.Background) { + switch background { + case .default: + return nil + case .solidColor(let color), .hexColor(let color): + guard let solidColor = SolidColorBackground(color) else { + return nil + } + self = .solidColor(solidColor) + case .gradient(let gradient): + guard let gradient = GradientBackground(rawValue: gradient) else { + return nil + } + self = .gradient(gradient) + case .userImage(let userImage): + self = .userImage(.init(fileName: userImage.id, colorScheme: .init(userImage.colorScheme))) + } + } +} + extension NewTabPageUserScript.UserImage { init(_ userBackgroundImage: UserBackgroundImage) { self.init( @@ -67,6 +109,17 @@ extension NewTabPageUserScript.UserImage { } } +extension ColorScheme { + init(_ theme: NewTabPageUserScript.Theme) { + switch theme { + case .dark: + self = .dark + case .light: + self = .light + } + } +} + extension NewTabPageUserScript.Theme { init(_ colorScheme: ColorScheme) { switch colorScheme { diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 32f305a916..6d1e3514e8 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -44,6 +44,7 @@ extension NewTabPageActionsManager { self.init(scriptClients: [ NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences, customBackgroundProvider: customizationProvider), + NewTabPageCustomBackgroundClient(model: customizationProvider), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), NewTabPageNextStepsCardsClient(model: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener())), NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: Int(Favicon.SizeCategory.medium.rawValue)), diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift index bf2ecf6b2e..94522da94d 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -23,6 +23,11 @@ import WebKit public protocol NewTabPageCustomBackgroundProviding: AnyObject { var customizerData: NewTabPageUserScript.CustomizerData { get } + var backgroundPublisher: AnyPublisher { get } + + var background: NewTabPageUserScript.Background { get set } + + @MainActor func presentUploadDialog() async } public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @@ -34,6 +39,14 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { public init(model: NewTabPageCustomBackgroundProviding) { self.model = model + + model.backgroundPublisher + .sink { [weak self] background in + Task { @MainActor in + self?.notifyBackgroundUpdated(background) + } + } + .store(in: &cancellables) } enum MessageName: String, CaseIterable { @@ -62,6 +75,10 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func setBackground(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let data: NewTabPageUserScript.BackgroundData = DecodableHelper.decode(from: params) else { + return nil + } + model.background = data.background return nil } @@ -72,8 +89,15 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func upload(params: Any, original: WKScriptMessage) async throws -> Encodable? { + await model.presentUploadDialog() return nil } + + @MainActor + private func notifyBackgroundUpdated(_ background: NewTabPageUserScript.Background) { + print(String(data: try! JSONEncoder().encode(NewTabPageUserScript.BackgroundData(background: background)), encoding: .utf8)!) + pushMessage(named: MessageName.onBackgroundUpdate.rawValue, params: background) + } } extension NewTabPageCustomBackgroundClient { diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index 846fcded49..563cbc1177 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -277,11 +277,15 @@ extension NewTabPageUserScript { } } - public enum Theme: String, Encodable { + public enum Theme: String, Codable { case dark, light } - public enum Background: Encodable, Equatable { + struct BackgroundData: Codable, Equatable { + let background: Background + } + + public enum Background: Codable, Equatable { case `default` case solidColor(String) case hexColor(String) @@ -298,9 +302,9 @@ extension NewTabPageUserScript { case .default: return "default" case .solidColor: - return "solidColor" + return "color" case .hexColor: - return "hexColor" + return "hex" case .gradient: return "gradient" case .userImage: @@ -308,21 +312,39 @@ extension NewTabPageUserScript { } } + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + let kind = try container.decode(String.self, forKey: CodingKeys.kind) + switch kind { + case "color", "hex": + let value = try container.decode(String.self, forKey: CodingKeys.value) + self = .solidColor(value) + case "gradient": + let value = try container.decode(String.self, forKey: CodingKeys.value) + self = .gradient(value) + case "userImage": + let value = try container.decode(UserImage.self, forKey: CodingKeys.value) + self = .userImage(value) + default: + self = .default + } + } + public func encode(to encoder: any Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: NewTabPageUserScript.Background.CodingKeys.self) - try container.encode(kind, forKey: NewTabPageUserScript.Background.CodingKeys.kind) + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encode(kind, forKey: CodingKeys.kind) switch self { case .default: break case .solidColor(let value), .hexColor(let value), .gradient(let value): - try container.encode(value, forKey: NewTabPageUserScript.Background.CodingKeys.value) + try container.encode(value, forKey: CodingKeys.value) case .userImage(let image): - try container.encode(image, forKey: NewTabPageUserScript.Background.CodingKeys.value) + try container.encode(image, forKey: CodingKeys.value) } } } - public struct UserImage: Encodable, Equatable { + public struct UserImage: Codable, Equatable { public let colorScheme: Theme public let id: String public let src: String From 96e694a029f8dbb573bb1cfed60bb21671d8d884 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 6 Dec 2024 21:14:41 +0100 Subject: [PATCH 28/62] Implement theme changing and deleting images --- .../HomePageSettingsModel+NewTabPage.swift | 35 +++++++++++++++++++ .../NewTabPageCustomBackgroundClient.swift | 28 +++++++++++++-- .../NewTabPageConfigurationClient.swift | 27 ++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift index 98a09f3ab3..64a6ca4b4f 100644 --- a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift @@ -52,10 +52,32 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding .eraseToAnyPublisher() } + var theme: NewTabPageUserScript.Theme? { + get { + .init(appearancePreferences.currentThemeName) + } + set { + appearancePreferences.currentThemeName = .init(newValue) + } + } + + var themePublisher: AnyPublisher { + appearancePreferences.$currentThemeName.dropFirst().removeDuplicates() + .map(NewTabPageUserScript.Theme.init) + .eraseToAnyPublisher() + } + @MainActor func presentUploadDialog() async{ await homePageSettingsModel.addNewImage() } + + func deleteImage(with imageID: String) async { + guard let image = homePageSettingsModel.availableUserBackgroundImages.first(where: { $0.id == imageID }) else { + return + } + homePageSettingsModel.customImagesManager?.deleteImage(image) + } } extension NewTabPageUserScript.Background { @@ -120,6 +142,19 @@ extension ColorScheme { } } +extension ThemeName { + init(_ theme: NewTabPageUserScript.Theme?) { + switch theme { + case .dark: + self = .dark + case .light: + self = .light + default: + self = .systemDefault + } + } +} + extension NewTabPageUserScript.Theme { init(_ colorScheme: ColorScheme) { switch colorScheme { diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift index 94522da94d..ed5e390f49 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -23,11 +23,15 @@ import WebKit public protocol NewTabPageCustomBackgroundProviding: AnyObject { var customizerData: NewTabPageUserScript.CustomizerData { get } - var backgroundPublisher: AnyPublisher { get } var background: NewTabPageUserScript.Background { get set } + var backgroundPublisher: AnyPublisher { get } + + var theme: NewTabPageUserScript.Theme? { get set } + var themePublisher: AnyPublisher { get } @MainActor func presentUploadDialog() async + func deleteImage(with imageID: String) async } public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @@ -47,6 +51,14 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { } } .store(in: &cancellables) + + model.themePublisher + .sink { [weak self] theme in + Task { @MainActor in + self?.notifyThemeUpdated(theme) + } + } + .store(in: &cancellables) } enum MessageName: String, CaseIterable { @@ -70,6 +82,10 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor func deleteImage(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let data: NewTabPageUserScript.DeleteImageData = DecodableHelper.decode(from: params) else { + return nil + } + await model.deleteImage(with: data.id) return nil } @@ -84,6 +100,10 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func setTheme(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let data: NewTabPageUserScript.ThemeData = DecodableHelper.decode(from: params) else { + return nil + } + model.theme = data.theme return nil } @@ -95,9 +115,13 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func notifyBackgroundUpdated(_ background: NewTabPageUserScript.Background) { - print(String(data: try! JSONEncoder().encode(NewTabPageUserScript.BackgroundData(background: background)), encoding: .utf8)!) pushMessage(named: MessageName.onBackgroundUpdate.rawValue, params: background) } + + @MainActor + private func notifyThemeUpdated(_ theme: NewTabPageUserScript.Theme?) { + pushMessage(named: MessageName.onThemeUpdate.rawValue, params: NewTabPageUserScript.ThemeData(theme: theme)) + } } extension NewTabPageCustomBackgroundClient { diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index 563cbc1177..05b96ceb55 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -277,6 +277,29 @@ extension NewTabPageUserScript { } } + public struct ThemeData: Codable, Equatable { + let theme: Theme? + + enum CodingKeys: CodingKey { + case theme + } + + public init(theme: Theme?) { + self.theme = theme + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: NewTabPageUserScript.ThemeData.CodingKeys.self) + try container.encodeIfPresent(self.theme?.rawValue ?? "system", forKey: NewTabPageUserScript.ThemeData.CodingKeys.theme) + } + + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: NewTabPageUserScript.ThemeData.CodingKeys.self) + let themeRawValue = try container.decode(String.self, forKey: NewTabPageUserScript.ThemeData.CodingKeys.theme) + theme = Theme(rawValue: themeRawValue) + } + } + public enum Theme: String, Codable { case dark, light } @@ -357,4 +380,8 @@ extension NewTabPageUserScript { self.thumb = thumb } } + + struct DeleteImageData: Codable, Equatable { + let id: String + } } From d7cf983ca4ed9d0af835587149f20454c1e6bd65 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 9 Dec 2024 11:49:51 +0100 Subject: [PATCH 29/62] Implement handling uploading images --- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- .../HomePageSettingsModel+NewTabPage.swift | 6 ++++++ .../NewTabPageCustomBackgroundClient.swift | 15 +++++++++++++++ .../NewTabPageConfigurationClient.swift | 4 ++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 27bb50441a..5239002c72 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/duckduckgo/BrowserServicesKit", "state": { "branch": null, - "revision": "f0755fbb3309c93c8490dc8bbdfb7e2e7613bef6", - "version": "217.0.2" + "revision": "a404b05cc0ec6b7b3e346804c1e0970dd91c0634", + "version": "218.0.2" } }, { @@ -66,7 +66,7 @@ }, { "package": "Lottie", - "repositoryURL": "https://github.com/airbnb/lottie-spm", + "repositoryURL": "https://github.com/airbnb/lottie-spm.git", "state": { "branch": null, "revision": "1d29eccc24cc8b75bff9f6804155112c0ffc9605", diff --git a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift index 64a6ca4b4f..1ded39ba0e 100644 --- a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift @@ -67,6 +67,12 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding .eraseToAnyPublisher() } + var userImagesPublisher: AnyPublisher<[NewTabPageUserScript.UserImage], Never> { + homePageSettingsModel.$availableUserBackgroundImages.dropFirst().removeDuplicates() + .map { $0.map(NewTabPageUserScript.UserImage.init) } + .eraseToAnyPublisher() + } + @MainActor func presentUploadDialog() async{ await homePageSettingsModel.addNewImage() diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift index ed5e390f49..eb00bde8d5 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -30,6 +30,8 @@ public protocol NewTabPageCustomBackgroundProviding: AnyObject { var theme: NewTabPageUserScript.Theme? { get set } var themePublisher: AnyPublisher { get } + var userImagesPublisher: AnyPublisher<[NewTabPageUserScript.UserImage], Never> { get } + @MainActor func presentUploadDialog() async func deleteImage(with imageID: String) async } @@ -59,6 +61,14 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { } } .store(in: &cancellables) + + model.userImagesPublisher + .sink { [weak self] images in + Task { @MainActor in + self?.notifyImagesUpdated(images) + } + } + .store(in: &cancellables) } enum MessageName: String, CaseIterable { @@ -122,6 +132,11 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { private func notifyThemeUpdated(_ theme: NewTabPageUserScript.Theme?) { pushMessage(named: MessageName.onThemeUpdate.rawValue, params: NewTabPageUserScript.ThemeData(theme: theme)) } + + @MainActor + private func notifyImagesUpdated(_ images: [NewTabPageUserScript.UserImage]) { + pushMessage(named: MessageName.onImagesUpdate.rawValue, params: NewTabPageUserScript.UserImagesData(userImages: images)) + } } extension NewTabPageCustomBackgroundClient { diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index 05b96ceb55..1b416d1bda 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -367,6 +367,10 @@ extension NewTabPageUserScript { } } + struct UserImagesData: Codable, Equatable { + let userImages: [UserImage] + } + public struct UserImage: Codable, Equatable { public let colorScheme: Theme public let id: String From 258db7ae110478bd829a88d830a23b74052045f6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 13:00:52 +0100 Subject: [PATCH 30/62] Update a comment in DuckURLSchemeHandler --- DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift index 8d0af874b0..455cb03f96 100644 --- a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift @@ -201,9 +201,8 @@ private extension DuckURLSchemeHandler { } /** - * Favicon URL has the format of `duck://favicon/`. - * Calling `requestURL.path` drops leading `duck://favicon` and automatically - * handles percent-encoding. We only need to drop the leading forward slash to get the favicon URL. + * Custom Background image has the format of `duck://new-tab/background/images/`. + * Custom Background image thumbnail has the format of `duck://new-tab/background/thumbnails/`. */ let fileName = requestURL.lastPathComponent From ce783ef1236afef111a5c8a0b585b730da991cd8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 13:15:55 +0100 Subject: [PATCH 31/62] Add NewTabPageDataModel enum to gather all data models --- .../HomePageSettingsModel+NewTabPage.swift | 32 ++-- .../NewTabPageCustomBackgroundClient.swift | 47 ++---- ...NewTabPageDataModel+CustomBackground.swift | 159 ++++++++++++++++++ .../NewTabPageConfigurationClient.swift | 140 +-------------- .../NewTabPage/NewTabPageDataModel.swift | 20 +++ 5 files changed, 211 insertions(+), 187 deletions(-) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift diff --git a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift index 1ded39ba0e..7581d04246 100644 --- a/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/HomePageSettingsModel+NewTabPage.swift @@ -29,15 +29,15 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding self.appearancePreferences = appearancePreferences } - var customizerData: NewTabPageUserScript.CustomizerData { + var customizerData: NewTabPageDataModel.CustomizerData { .init( background: .init(homePageSettingsModel.customBackground), theme: .init(appearancePreferences.currentThemeName), - userImages: homePageSettingsModel.availableUserBackgroundImages.map(NewTabPageUserScript.UserImage.init) + userImages: homePageSettingsModel.availableUserBackgroundImages.map(NewTabPageDataModel.UserImage.init) ) } - var background: NewTabPageUserScript.Background { + var background: NewTabPageDataModel.Background { get { .init(homePageSettingsModel.customBackground) } @@ -46,13 +46,13 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding } } - var backgroundPublisher: AnyPublisher { + var backgroundPublisher: AnyPublisher { homePageSettingsModel.$customBackground.dropFirst().removeDuplicates() - .map(NewTabPageUserScript.Background.init) + .map(NewTabPageDataModel.Background.init) .eraseToAnyPublisher() } - var theme: NewTabPageUserScript.Theme? { + var theme: NewTabPageDataModel.Theme? { get { .init(appearancePreferences.currentThemeName) } @@ -61,15 +61,15 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding } } - var themePublisher: AnyPublisher { + var themePublisher: AnyPublisher { appearancePreferences.$currentThemeName.dropFirst().removeDuplicates() - .map(NewTabPageUserScript.Theme.init) + .map(NewTabPageDataModel.Theme.init) .eraseToAnyPublisher() } - var userImagesPublisher: AnyPublisher<[NewTabPageUserScript.UserImage], Never> { + var userImagesPublisher: AnyPublisher<[NewTabPageDataModel.UserImage], Never> { homePageSettingsModel.$availableUserBackgroundImages.dropFirst().removeDuplicates() - .map { $0.map(NewTabPageUserScript.UserImage.init) } + .map { $0.map(NewTabPageDataModel.UserImage.init) } .eraseToAnyPublisher() } @@ -86,7 +86,7 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding } } -extension NewTabPageUserScript.Background { +extension NewTabPageDataModel.Background { init(_ customBackground: CustomBackground?) { switch customBackground { case .gradient(let gradient): @@ -106,7 +106,7 @@ extension NewTabPageUserScript.Background { } extension CustomBackground { - init?(_ background: NewTabPageUserScript.Background) { + init?(_ background: NewTabPageDataModel.Background) { switch background { case .default: return nil @@ -126,7 +126,7 @@ extension CustomBackground { } } -extension NewTabPageUserScript.UserImage { +extension NewTabPageDataModel.UserImage { init(_ userBackgroundImage: UserBackgroundImage) { self.init( colorScheme: .init(userBackgroundImage.colorScheme), @@ -138,7 +138,7 @@ extension NewTabPageUserScript.UserImage { } extension ColorScheme { - init(_ theme: NewTabPageUserScript.Theme) { + init(_ theme: NewTabPageDataModel.Theme) { switch theme { case .dark: self = .dark @@ -149,7 +149,7 @@ extension ColorScheme { } extension ThemeName { - init(_ theme: NewTabPageUserScript.Theme?) { + init(_ theme: NewTabPageDataModel.Theme?) { switch theme { case .dark: self = .dark @@ -161,7 +161,7 @@ extension ThemeName { } } -extension NewTabPageUserScript.Theme { +extension NewTabPageDataModel.Theme { init(_ colorScheme: ColorScheme) { switch colorScheme { case .dark: diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift index eb00bde8d5..c29491574d 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -22,15 +22,15 @@ import UserScript import WebKit public protocol NewTabPageCustomBackgroundProviding: AnyObject { - var customizerData: NewTabPageUserScript.CustomizerData { get } + var customizerData: NewTabPageDataModel.CustomizerData { get } - var background: NewTabPageUserScript.Background { get set } - var backgroundPublisher: AnyPublisher { get } + var background: NewTabPageDataModel.Background { get set } + var backgroundPublisher: AnyPublisher { get } - var theme: NewTabPageUserScript.Theme? { get set } - var themePublisher: AnyPublisher { get } + var theme: NewTabPageDataModel.Theme? { get set } + var themePublisher: AnyPublisher { get } - var userImagesPublisher: AnyPublisher<[NewTabPageUserScript.UserImage], Never> { get } + var userImagesPublisher: AnyPublisher<[NewTabPageDataModel.UserImage], Never> { get } @MainActor func presentUploadDialog() async func deleteImage(with imageID: String) async @@ -92,7 +92,7 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor func deleteImage(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let data: NewTabPageUserScript.DeleteImageData = DecodableHelper.decode(from: params) else { + guard let data: NewTabPageDataModel.DeleteImageData = DecodableHelper.decode(from: params) else { return nil } await model.deleteImage(with: data.id) @@ -101,7 +101,7 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func setBackground(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let data: NewTabPageUserScript.BackgroundData = DecodableHelper.decode(from: params) else { + guard let data: NewTabPageDataModel.BackgroundData = DecodableHelper.decode(from: params) else { return nil } model.background = data.background @@ -110,7 +110,7 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func setTheme(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let data: NewTabPageUserScript.ThemeData = DecodableHelper.decode(from: params) else { + guard let data: NewTabPageDataModel.ThemeData = DecodableHelper.decode(from: params) else { return nil } model.theme = data.theme @@ -124,36 +124,17 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { } @MainActor - private func notifyBackgroundUpdated(_ background: NewTabPageUserScript.Background) { + private func notifyBackgroundUpdated(_ background: NewTabPageDataModel.Background) { pushMessage(named: MessageName.onBackgroundUpdate.rawValue, params: background) } @MainActor - private func notifyThemeUpdated(_ theme: NewTabPageUserScript.Theme?) { - pushMessage(named: MessageName.onThemeUpdate.rawValue, params: NewTabPageUserScript.ThemeData(theme: theme)) + private func notifyThemeUpdated(_ theme: NewTabPageDataModel.Theme?) { + pushMessage(named: MessageName.onThemeUpdate.rawValue, params: NewTabPageDataModel.ThemeData(theme: theme)) } @MainActor - private func notifyImagesUpdated(_ images: [NewTabPageUserScript.UserImage]) { - pushMessage(named: MessageName.onImagesUpdate.rawValue, params: NewTabPageUserScript.UserImagesData(userImages: images)) - } -} - -extension NewTabPageCustomBackgroundClient { - - public enum CardID: String, Codable { - case bringStuff - case defaultApp - case emailProtection - case duckplayer - case addAppToDockMac - } - - struct Card: Codable, Equatable { - let id: CardID - } - - struct NextStepsData: Codable, Equatable { - public let content: [Card]? + private func notifyImagesUpdated(_ images: [NewTabPageDataModel.UserImage]) { + pushMessage(named: MessageName.onImagesUpdate.rawValue, params: NewTabPageDataModel.UserImagesData(userImages: images)) } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift new file mode 100644 index 0000000000..441eba8986 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift @@ -0,0 +1,159 @@ +// +// NewTabPageDataModel+CustomBackground.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. +// + +public extension NewTabPageDataModel { + struct CustomizerData: Encodable, Equatable { + public let background: Background + public let theme: Theme? + public let userImages: [UserImage] + + public init(background: Background, theme: Theme?, userImages: [UserImage]) { + self.background = background + self.theme = theme + self.userImages = userImages + } + + enum CodingKeys: CodingKey { + case background + case theme + case userImages + } + + public func encode(to encoder: any Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.background, forKey: CodingKeys.background) + try container.encode(self.theme?.rawValue ?? "system", forKey: CodingKeys.theme) + try container.encode(self.userImages, forKey: CodingKeys.userImages) + } + } + + struct ThemeData: Codable, Equatable { + let theme: Theme? + + enum CodingKeys: CodingKey { + case theme + } + + public init(theme: Theme?) { + self.theme = theme + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(self.theme?.rawValue ?? "system", forKey: CodingKeys.theme) + } + + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + let themeRawValue = try container.decode(String.self, forKey: CodingKeys.theme) + theme = Theme(rawValue: themeRawValue) + } + } + + enum Theme: String, Codable { + case dark, light + } + + enum Background: Codable, Equatable { + case `default` + case solidColor(String) + case hexColor(String) + case gradient(String) + case userImage(UserImage) + + enum CodingKeys: CodingKey { + case kind + case value + } + + var kind: String { + switch self { + case .default: + return "default" + case .solidColor: + return "color" + case .hexColor: + return "hex" + case .gradient: + return "gradient" + case .userImage: + return "userImage" + } + } + + public init(from decoder: any Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + let kind = try container.decode(String.self, forKey: CodingKeys.kind) + switch kind { + case "color", "hex": + let value = try container.decode(String.self, forKey: CodingKeys.value) + self = .solidColor(value) + case "gradient": + let value = try container.decode(String.self, forKey: CodingKeys.value) + self = .gradient(value) + case "userImage": + let value = try container.decode(UserImage.self, forKey: CodingKeys.value) + self = .userImage(value) + default: + self = .default + } + } + + public func encode(to encoder: any Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encode(kind, forKey: CodingKeys.kind) + switch self { + case .default: + break + case .solidColor(let value), .hexColor(let value), .gradient(let value): + try container.encode(value, forKey: CodingKeys.value) + case .userImage(let image): + try container.encode(image, forKey: CodingKeys.value) + } + } + } + + struct UserImage: Codable, Equatable { + public let colorScheme: Theme + public let id: String + public let src: String + public let thumb: String + + public init(colorScheme: Theme, id: String, src: String, thumb: String) { + self.colorScheme = colorScheme + self.id = id + self.src = src + self.thumb = thumb + } + } +} + +extension NewTabPageDataModel { + + struct BackgroundData: Codable, Equatable { + let background: Background + } + + struct UserImagesData: Codable, Equatable { + let userImages: [UserImage] + } + + struct DeleteImageData: Codable, Equatable { + let id: String + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift index 1b416d1bda..29457119b6 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift @@ -206,7 +206,7 @@ extension NewTabPageUserScript { var locale: String var platform: Platform var settings: Settings - var customizer: CustomizerData + var customizer: NewTabPageDataModel.CustomizerData struct Widget: Encodable, Equatable { public var id: WidgetId @@ -251,141 +251,5 @@ extension NewTabPageUserScript { } } } - - public struct CustomizerData: Encodable, Equatable { - public let background: Background - public let theme: Theme? - public let userImages: [UserImage] - - public init(background: Background, theme: Theme?, userImages: [UserImage]) { - self.background = background - self.theme = theme - self.userImages = userImages - } - - enum CodingKeys: CodingKey { - case background - case theme - case userImages - } - - public func encode(to encoder: any Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: NewTabPageUserScript.CustomizerData.CodingKeys.self) - try container.encode(self.background, forKey: NewTabPageUserScript.CustomizerData.CodingKeys.background) - try container.encode(self.theme?.rawValue ?? "system", forKey: NewTabPageUserScript.CustomizerData.CodingKeys.theme) - try container.encode(self.userImages, forKey: NewTabPageUserScript.CustomizerData.CodingKeys.userImages) - } - } - - public struct ThemeData: Codable, Equatable { - let theme: Theme? - - enum CodingKeys: CodingKey { - case theme - } - - public init(theme: Theme?) { - self.theme = theme - } - - public func encode(to encoder: any Encoder) throws { - var container = encoder.container(keyedBy: NewTabPageUserScript.ThemeData.CodingKeys.self) - try container.encodeIfPresent(self.theme?.rawValue ?? "system", forKey: NewTabPageUserScript.ThemeData.CodingKeys.theme) - } - - public init(from decoder: any Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: NewTabPageUserScript.ThemeData.CodingKeys.self) - let themeRawValue = try container.decode(String.self, forKey: NewTabPageUserScript.ThemeData.CodingKeys.theme) - theme = Theme(rawValue: themeRawValue) - } - } - - public enum Theme: String, Codable { - case dark, light - } - - struct BackgroundData: Codable, Equatable { - let background: Background - } - - public enum Background: Codable, Equatable { - case `default` - case solidColor(String) - case hexColor(String) - case gradient(String) - case userImage(UserImage) - - enum CodingKeys: CodingKey { - case kind - case value - } - - var kind: String { - switch self { - case .default: - return "default" - case .solidColor: - return "color" - case .hexColor: - return "hex" - case .gradient: - return "gradient" - case .userImage: - return "userImage" - } - } - - public init(from decoder: any Decoder) throws { - let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - let kind = try container.decode(String.self, forKey: CodingKeys.kind) - switch kind { - case "color", "hex": - let value = try container.decode(String.self, forKey: CodingKeys.value) - self = .solidColor(value) - case "gradient": - let value = try container.decode(String.self, forKey: CodingKeys.value) - self = .gradient(value) - case "userImage": - let value = try container.decode(UserImage.self, forKey: CodingKeys.value) - self = .userImage(value) - default: - self = .default - } - } - - public func encode(to encoder: any Encoder) throws { - var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) - try container.encode(kind, forKey: CodingKeys.kind) - switch self { - case .default: - break - case .solidColor(let value), .hexColor(let value), .gradient(let value): - try container.encode(value, forKey: CodingKeys.value) - case .userImage(let image): - try container.encode(image, forKey: CodingKeys.value) - } - } - } - - struct UserImagesData: Codable, Equatable { - let userImages: [UserImage] - } - - public struct UserImage: Codable, Equatable { - public let colorScheme: Theme - public let id: String - public let src: String - public let thumb: String - - public init(colorScheme: Theme, id: String, src: String, thumb: String) { - self.colorScheme = colorScheme - self.id = id - self.src = src - self.thumb = thumb - } - } - - struct DeleteImageData: Codable, Equatable { - let id: String - } } + diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift new file mode 100644 index 0000000000..0fda4bb512 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift @@ -0,0 +1,20 @@ +// +// NewTabPageDataModel.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. +// + +public enum NewTabPageDataModel {} + From dc6977a13f17720794b8f06589fccc58a9e0ad78 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 13:23:35 +0100 Subject: [PATCH 32/62] Move Configuration models to NewTabPageDataModel --- .../NewTabPageConfigurationClient.swift | 80 ++--------------- .../NewTabPageDataModel+Configuration.swift | 88 +++++++++++++++++++ .../NewTabPageCustomBackgroundClient.swift | 2 +- ...NewTabPageDataModel+CustomBackground.swift | 3 + 4 files changed, 97 insertions(+), 76 deletions(-) rename LocalPackages/NewTabPage/Sources/NewTabPage/{ => Configuration}/NewTabPageConfigurationClient.swift (76%) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift similarity index 76% rename from LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift index 29457119b6..a85ebdfc09 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift @@ -77,7 +77,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { } private func notifyWidgetConfigsDidChange() { - let widgetConfigs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ + let widgetConfigs: [NewTabPageDataModel.NewTabPageConfiguration.WidgetConfig] = [ .init(id: .favorites, isVisible: sectionsVisibilityProvider.isFavoritesVisible), .init(id: .privacyStats, isVisible: sectionsVisibilityProvider.isPrivacyStatsVisible) ] @@ -87,7 +87,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { @MainActor private func showContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let params: NewTabPageUserScript.ContextMenuParams = DecodableHelper.decode(from: params) else { return nil } + guard let params: NewTabPageDataModel.ContextMenuParams = DecodableHelper.decode(from: params) else { return nil } let menu = NSMenu() @@ -118,7 +118,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { } @objc private func toggleVisibility(_ sender: NSMenuItem) { - switch sender.representedObject as? NewTabPageUserScript.WidgetId { + switch sender.representedObject as? NewTabPageDataModel.WidgetId { case .favorites: sectionsVisibilityProvider.isFavoritesVisible.toggle() case .privacyStats: @@ -137,7 +137,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { #endif let customizerData = customBackgroundProvider.customizerData - let config = NewTabPageUserScript.NewTabPageConfiguration( + let config = NewTabPageDataModel.NewTabPageConfiguration( widgets: [ .init(id: .rmf), .init(id: .nextSteps), @@ -159,7 +159,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { @MainActor private func widgetsSetConfig(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let widgetConfigs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = DecodableHelper.decode(from: params) else { + guard let widgetConfigs: [NewTabPageDataModel.NewTabPageConfiguration.WidgetConfig] = DecodableHelper.decode(from: params) else { return nil } for widgetConfig in widgetConfigs { @@ -183,73 +183,3 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { return nil } } - -extension NewTabPageUserScript { - - enum WidgetId: String, Codable { - case rmf, nextSteps, favorites, privacyStats - } - - struct ContextMenuParams: Codable { - let visibilityMenuItems: [ContextMenuItem] - - struct ContextMenuItem: Codable { - let id: WidgetId - let title: String - } - } - - struct NewTabPageConfiguration: Encodable { - var widgets: [Widget] - var widgetConfigs: [WidgetConfig] - var env: String - var locale: String - var platform: Platform - var settings: Settings - var customizer: NewTabPageDataModel.CustomizerData - - struct Widget: Encodable, Equatable { - public var id: WidgetId - } - - struct WidgetConfig: Codable, Equatable { - - enum WidgetVisibility: String, Codable { - case visible, hidden - - var isVisible: Bool { - self == .visible - } - } - - init(id: WidgetId, isVisible: Bool) { - self.id = id - self.visibility = isVisible ? .visible : .hidden - } - - var id: WidgetId - var visibility: WidgetVisibility - } - - struct Platform: Encodable, Equatable { - var name: String - } - - struct Settings: Encodable, Equatable { - let customizerDrawer: Setting - } - - struct Setting: Encodable, Equatable { - let state: BooleanSetting - } - - enum BooleanSetting: String, Encodable { - case enabled, disabled - - var isEnabled: Bool { - self == .enabled - } - } - } -} - diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift new file mode 100644 index 0000000000..91a5973953 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift @@ -0,0 +1,88 @@ +// +// NewTabPageDataModel+Configuration.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 + +extension NewTabPageDataModel { + + enum WidgetId: String, Codable { + case rmf, nextSteps, favorites, privacyStats + } + + struct ContextMenuParams: Codable { + let visibilityMenuItems: [ContextMenuItem] + + struct ContextMenuItem: Codable { + let id: WidgetId + let title: String + } + } + + struct NewTabPageConfiguration: Encodable { + var widgets: [Widget] + var widgetConfigs: [WidgetConfig] + var env: String + var locale: String + var platform: Platform + var settings: Settings + var customizer: NewTabPageDataModel.CustomizerData + + struct Widget: Encodable, Equatable { + public var id: WidgetId + } + + struct WidgetConfig: Codable, Equatable { + + enum WidgetVisibility: String, Codable { + case visible, hidden + + var isVisible: Bool { + self == .visible + } + } + + init(id: WidgetId, isVisible: Bool) { + self.id = id + self.visibility = isVisible ? .visible : .hidden + } + + var id: WidgetId + var visibility: WidgetVisibility + } + + struct Platform: Encodable, Equatable { + var name: String + } + + struct Settings: Encodable, Equatable { + let customizerDrawer: Setting + } + + struct Setting: Encodable, Equatable { + let state: BooleanSetting + } + + enum BooleanSetting: String, Encodable { + case enabled, disabled + + var isEnabled: Bool { + self == .enabled + } + } + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift index c29491574d..a635b3d2b0 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -125,7 +125,7 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { @MainActor private func notifyBackgroundUpdated(_ background: NewTabPageDataModel.Background) { - pushMessage(named: MessageName.onBackgroundUpdate.rawValue, params: background) + pushMessage(named: MessageName.onBackgroundUpdate.rawValue, params: NewTabPageDataModel.BackgroundData(background: background)) } @MainActor diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift index 441eba8986..34abf553f3 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift @@ -16,7 +16,10 @@ // limitations under the License. // +import Foundation + public extension NewTabPageDataModel { + struct CustomizerData: Encodable, Equatable { public let background: Background public let theme: Theme? From 91486bd90652488ec151b59892480c9c84693900 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 16:46:05 +0100 Subject: [PATCH 33/62] Move Favorites models to NewTabPageDataModel --- .../NewTabPageDataModel+Favorites.swift | 87 +++++++++++++++++++ .../Favorites/NewTabPageFavoritesClient.swift | 82 ++--------------- 2 files changed, 94 insertions(+), 75 deletions(-) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageDataModel+Favorites.swift diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageDataModel+Favorites.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageDataModel+Favorites.swift new file mode 100644 index 0000000000..d57d112b31 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageDataModel+Favorites.swift @@ -0,0 +1,87 @@ +// +// NewTabPageDataModel+Favorites.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 + +extension NewTabPageDataModel { + + struct FavoritesContextMenuAction: Codable { + let id: String + } + + struct FavoritesOpenAction: Codable { + let id: String + let url: String + } + + struct FavoritesMoveAction: Codable { + let id: String + let fromIndex: Int + let targetIndex: Int + } + + struct FavoritesConfig: Codable { + let expansion: Expansion + + enum Expansion: String, Codable { + case expanded, collapsed + } + } + + struct FavoritesData: Encodable { + let favorites: [Favorite] + } + + struct Favorite: Encodable, Equatable { + let favicon: FavoriteFavicon? + let id: String + let title: String + let url: String + + init(id: String, title: String, url: String, favicon: NewTabPageDataModel.FavoriteFavicon? = nil) { + self.id = id + self.title = title + self.url = url + self.favicon = favicon + } + + @MainActor + init(_ bookmark: NewTabPageFavorite, preferredFaviconSize: Int, onFaviconMissing: () -> Void) { + id = bookmark.id + title = bookmark.title + url = bookmark.url + + if let url = bookmark.urlObject, let duckFaviconURL = URL.duckFavicon(for: url) { + favicon = FavoriteFavicon(maxAvailableSize: preferredFaviconSize, src: duckFaviconURL.absoluteString) + } else { + onFaviconMissing() + favicon = nil + } + } + } + + struct FavoriteFavicon: Encodable, Equatable { + let maxAvailableSize: Int + let src: String + + init(maxAvailableSize: Int, src: String) { + self.maxAvailableSize = maxAvailableSize + self.src = src + } + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift index 7aa3c1e977..628d32a2e5 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Favorites/NewTabPageFavoritesClient.swift @@ -99,17 +99,17 @@ public final class NewTabPageFavoritesClient: NewTa @MainActor private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let favorites = favoritesModel.favorites.map { - NewTabPageFavoritesClient.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) + NewTabPageDataModel.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) } - return NewTabPageFavoritesClient.FavoritesData(favorites: favorites) + return NewTabPageDataModel.FavoritesData(favorites: favorites) } @MainActor private func notifyDataUpdated(_ favorites: [NewTabPageFavorite]) { let favorites = favoritesModel.favorites.map { - NewTabPageFavoritesClient.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) + NewTabPageDataModel.Favorite($0, preferredFaviconSize: preferredFaviconSize, onFaviconMissing: favoritesModel.onFaviconMissing) } - pushMessage(named: MessageName.onDataUpdate.rawValue, params: NewTabPageFavoritesClient.FavoritesData(favorites: favorites)) + pushMessage(named: MessageName.onDataUpdate.rawValue, params: NewTabPageDataModel.FavoritesData(favorites: favorites)) } @MainActor @@ -121,7 +121,7 @@ public final class NewTabPageFavoritesClient: NewTa @MainActor private func move(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let action: NewTabPageFavoritesClient.FavoritesMoveAction = DecodableHelper.decode(from: params) else { + guard let action: NewTabPageDataModel.FavoritesMoveAction = DecodableHelper.decode(from: params) else { return nil } favoritesModel.moveFavorite(withID: action.id, fromIndex: action.fromIndex, toIndex: action.targetIndex) @@ -130,7 +130,7 @@ public final class NewTabPageFavoritesClient: NewTa @MainActor private func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let action: NewTabPageFavoritesClient.FavoritesOpenAction = DecodableHelper.decode(from: params) else { + guard let action: NewTabPageDataModel.FavoritesOpenAction = DecodableHelper.decode(from: params) else { return nil } favoritesModel.openFavorite(withURL: action.url) @@ -139,7 +139,7 @@ public final class NewTabPageFavoritesClient: NewTa @MainActor private func openContextMenu(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let contextMenuAction: NewTabPageFavoritesClient.FavoritesContextMenuAction = DecodableHelper.decode(from: params) else { + guard let contextMenuAction: NewTabPageDataModel.FavoritesContextMenuAction = DecodableHelper.decode(from: params) else { return nil } favoritesModel.showContextMenu(for: contextMenuAction.id) @@ -147,74 +147,6 @@ public final class NewTabPageFavoritesClient: NewTa } } -public extension NewTabPageFavoritesClient { - - struct FavoritesContextMenuAction: Codable { - let id: String - } - - struct FavoritesOpenAction: Codable { - let id: String - let url: String - } - - struct FavoritesMoveAction: Codable { - let id: String - let fromIndex: Int - let targetIndex: Int - } - - struct FavoritesConfig: Codable { - let expansion: Expansion - - enum Expansion: String, Codable { - case expanded, collapsed - } - } - - struct FavoritesData: Encodable { - let favorites: [Favorite] - } - - struct Favorite: Encodable, Equatable { - let favicon: FavoriteFavicon? - let id: String - let title: String - let url: String - - init(id: String, title: String, url: String, favicon: NewTabPageFavoritesClient.FavoriteFavicon? = nil) { - self.id = id - self.title = title - self.url = url - self.favicon = favicon - } - - @MainActor - init(_ bookmark: NewTabPageFavorite, preferredFaviconSize: Int, onFaviconMissing: () -> Void) { - id = bookmark.id - title = bookmark.title - url = bookmark.url - - if let url = bookmark.urlObject, let duckFaviconURL = URL.duckFavicon(for: url) { - favicon = FavoriteFavicon(maxAvailableSize: preferredFaviconSize, src: duckFaviconURL.absoluteString) - } else { - onFaviconMissing() - favicon = nil - } - } - } - - struct FavoriteFavicon: Encodable, Equatable { - let maxAvailableSize: Int - let src: String - - init(maxAvailableSize: Int, src: String) { - self.maxAvailableSize = maxAvailableSize - self.src = src - } - } -} - extension URL { static func duckFavicon(for faviconURL: URL) -> URL? { let encodedURL = faviconURL.absoluteString.percentEncoded(withAllowedCharacters: .urlPathAllowed) From 7628b087b3fd392d36fc08d65a0e2b6abc9b5f01 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 16:53:08 +0100 Subject: [PATCH 34/62] Move NextStepsCards models to NewTabPageDataModel --- .../NewTabPageNextStepsCardsProvider.swift | 22 +++++----- .../NewTabPageDataModel+NextStepsCards.swift | 41 ++++++++++++++++++ .../NewTabPageNextStepsCardsClient.swift | 43 ++++++------------- .../NewTabPageNextStepsCardsProviding.swift | 10 ++--- 4 files changed, 69 insertions(+), 47 deletions(-) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageDataModel+NextStepsCards.swift diff --git a/DuckDuckGo/NewTabPage/NewTabPageNextStepsCardsProvider.swift b/DuckDuckGo/NewTabPage/NewTabPageNextStepsCardsProvider.swift index b56558e98e..2187dbbe16 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageNextStepsCardsProvider.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageNextStepsCardsProvider.swift @@ -44,44 +44,44 @@ final class NewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding continueSetUpModel.shouldShowAllFeaturesPublisher.eraseToAnyPublisher() } - var cards: [NewTabPageNextStepsCardsClient.CardID] { + var cards: [NewTabPageDataModel.CardID] { guard !appearancePreferences.isContinueSetUpCardsViewOutdated else { return [] } - return continueSetUpModel.featuresMatrix.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } + return continueSetUpModel.featuresMatrix.flatMap { $0.map(NewTabPageDataModel.CardID.init) } } - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { + var cardsPublisher: AnyPublisher<[NewTabPageDataModel.CardID], Never> { let features = continueSetUpModel.$featuresMatrix.dropFirst().removeDuplicates() let cardsDidBecomeOutdated = appearancePreferences.$isContinueSetUpCardsViewOutdated.removeDuplicates() return Publishers.CombineLatest(features, cardsDidBecomeOutdated) - .map { features, isOutdated -> [NewTabPageNextStepsCardsClient.CardID] in + .map { features, isOutdated -> [NewTabPageDataModel.CardID] in guard !isOutdated else { return [] } - return features.flatMap { $0.map(NewTabPageNextStepsCardsClient.CardID.init) } + return features.flatMap { $0.map(NewTabPageDataModel.CardID.init) } } .eraseToAnyPublisher() } @MainActor - func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) { + func handleAction(for card: NewTabPageDataModel.CardID) { continueSetUpModel.performAction(for: .init(card)) } @MainActor - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { + func dismiss(_ card: NewTabPageDataModel.CardID) { continueSetUpModel.removeItem(for: .init(card)) } @MainActor - func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { + func willDisplayCards(_ cards: [NewTabPageDataModel.CardID]) { appearancePreferences.continueSetUpCardsViewDidAppear() fireAddToDockPixelIfNeeded(cards) } - private func fireAddToDockPixelIfNeeded(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { + private func fireAddToDockPixelIfNeeded(_ cards: [NewTabPageDataModel.CardID]) { guard cards.contains(.addAppToDockMac) else { return } @@ -92,7 +92,7 @@ final class NewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsProviding } extension HomePage.Models.FeatureType { - init(_ card: NewTabPageNextStepsCardsClient.CardID) { + init(_ card: NewTabPageDataModel.CardID) { switch card { case .bringStuff: self = .importBookmarksAndPasswords @@ -108,7 +108,7 @@ extension HomePage.Models.FeatureType { } } -extension NewTabPageNextStepsCardsClient.CardID { +extension NewTabPageDataModel.CardID { init(_ feature: HomePage.Models.FeatureType) { switch feature { case .duckplayer: diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageDataModel+NextStepsCards.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageDataModel+NextStepsCards.swift new file mode 100644 index 0000000000..b1097365a9 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageDataModel+NextStepsCards.swift @@ -0,0 +1,41 @@ +// +// NewTabPageDataModel+NextStepsCards.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 + +public extension NewTabPageDataModel { + + enum CardID: String, Codable { + case bringStuff + case defaultApp + case emailProtection + case duckplayer + case addAppToDockMac + } +} + +extension NewTabPageDataModel { + + struct Card: Codable, Equatable { + let id: CardID + } + + struct NextStepsData: Codable, Equatable { + public let content: [Card]? + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift index 46b11553bc..50fd73de61 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsClient.swift @@ -24,13 +24,13 @@ import WebKit public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { let model: NewTabPageNextStepsCardsProviding - let willDisplayCardsPublisher: AnyPublisher<[CardID], Never> + let willDisplayCardsPublisher: AnyPublisher<[NewTabPageDataModel.CardID], Never> public weak var userScriptsSource: NewTabPageUserScriptsSource? - private let willDisplayCardsSubject = PassthroughSubject<[CardID], Never>() - private let getDataSubject = PassthroughSubject<[CardID], Never>() + private let willDisplayCardsSubject = PassthroughSubject<[NewTabPageDataModel.CardID], Never>() + private let getDataSubject = PassthroughSubject<[NewTabPageDataModel.CardID], Never>() private let getConfigSubject = PassthroughSubject() - private let notifyDataUpdatedSubject = PassthroughSubject<[CardID], Never>() + private let notifyDataUpdatedSubject = PassthroughSubject<[NewTabPageDataModel.CardID], Never>() private let notifyConfigUpdatedSubject = PassthroughSubject() private var cancellables: Set = [] @@ -83,7 +83,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { // only notify about cards revealed by expanding the view (i.e. other than the first 2) let cardsOnConfigUpdated = notifyConfigUpdatedSubject .drop(untilOutputFrom: firstInitialCards) - .compactMap { [weak self] isViewExpanded -> [CardID]? in + .compactMap { [weak self] isViewExpanded -> [NewTabPageDataModel.CardID]? in guard let self, isViewExpanded, model.cards.count > 2 else { return nil } @@ -120,7 +120,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { @MainActor private func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let card: Card = DecodableHelper.decode(from: params) else { + guard let card: NewTabPageDataModel.Card = DecodableHelper.decode(from: params) else { return nil } model.handleAction(for: card.id) @@ -129,7 +129,7 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { @MainActor private func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let card: Card = DecodableHelper.decode(from: params) else { + guard let card: NewTabPageDataModel.Card = DecodableHelper.decode(from: params) else { return nil } model.dismiss(card.id) @@ -155,16 +155,16 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { @MainActor private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { let cardIDs = model.cards - let cards = cardIDs.map(Card.init(id:)) + let cards = cardIDs.map(NewTabPageDataModel.Card.init(id:)) getDataSubject.send(cardIDs) - return NextStepsData(content: cards.isEmpty ? nil : cards) + return NewTabPageDataModel.NextStepsData(content: cards.isEmpty ? nil : cards) } @MainActor - private func notifyDataUpdated(_ cardIDs: [CardID]) { - let cards = cardIDs.map(Card.init(id:)) - let params = NextStepsData(content: cards.isEmpty ? nil : cards) + private func notifyDataUpdated(_ cardIDs: [NewTabPageDataModel.CardID]) { + let cards = cardIDs.map(NewTabPageDataModel.Card.init(id:)) + let params = NewTabPageDataModel.NextStepsData(content: cards.isEmpty ? nil : cards) notifyDataUpdatedSubject.send(cardIDs) pushMessage(named: MessageName.onDataUpdate.rawValue, params: params) @@ -179,22 +179,3 @@ public final class NewTabPageNextStepsCardsClient: NewTabPageScriptClient { pushMessage(named: MessageName.onConfigUpdate.rawValue, params: config) } } - -extension NewTabPageNextStepsCardsClient { - - public enum CardID: String, Codable { - case bringStuff - case defaultApp - case emailProtection - case duckplayer - case addAppToDockMac - } - - struct Card: Codable, Equatable { - let id: CardID - } - - struct NextStepsData: Codable, Equatable { - public let content: [Card]? - } -} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift index a6843ef2ba..fbf71212af 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NextStepsCards/NewTabPageNextStepsCardsProviding.swift @@ -25,15 +25,15 @@ public protocol NewTabPageNextStepsCardsProviding: AnyObject { var isViewExpanded: Bool { get set } var isViewExpandedPublisher: AnyPublisher { get } - var cards: [NewTabPageNextStepsCardsClient.CardID] { get } - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { get } + var cards: [NewTabPageDataModel.CardID] { get } + var cardsPublisher: AnyPublisher<[NewTabPageDataModel.CardID], Never> { get } @MainActor - func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) + func handleAction(for card: NewTabPageDataModel.CardID) @MainActor - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) + func dismiss(_ card: NewTabPageDataModel.CardID) @MainActor - func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) + func willDisplayCards(_ cards: [NewTabPageDataModel.CardID]) } From 92214519efd8804ba3d3aa756c33037756ca9a4e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 17:24:37 +0100 Subject: [PATCH 35/62] Move PrivacyStats models to NewTabPageDataModel --- .../NewTabPageDataModel+PrivacyStats.swift | 40 +++++++++++++++++++ .../NewTabPagePrivacyStatsClient.swift | 21 ---------- .../NewTabPagePrivacyStatsModel.swift | 8 ++-- 3 files changed, 44 insertions(+), 25 deletions(-) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPageDataModel+PrivacyStats.swift diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPageDataModel+PrivacyStats.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPageDataModel+PrivacyStats.swift new file mode 100644 index 0000000000..61b76e5bcc --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPageDataModel+PrivacyStats.swift @@ -0,0 +1,40 @@ +// +// NewTabPageDataModel+PrivacyStats.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 + +extension NewTabPageDataModel { + + struct PrivacyStatsData: Encodable, Equatable { + let totalCount: Int64 + let trackerCompanies: [TrackerCompany] + + static func == (lhs: PrivacyStatsData, rhs: PrivacyStatsData) -> Bool { + lhs.totalCount == rhs.totalCount && Set(lhs.trackerCompanies) == Set(rhs.trackerCompanies) + } + } + + struct TrackerCompany: Encodable, Equatable, Hashable { + let count: Int64 + let displayName: String + + static func otherCompanies(count: Int64) -> TrackerCompany { + TrackerCompany(count: count, displayName: "__other__") + } + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift index f28582afc5..62fc3804b3 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsClient.swift @@ -111,24 +111,3 @@ public final class NewTabPagePrivacyStatsClient: NewTabPageScriptClient { return nil } } - -extension NewTabPagePrivacyStatsClient { - - struct PrivacyStatsData: Encodable, Equatable { - let totalCount: Int64 - let trackerCompanies: [TrackerCompany] - - static func == (lhs: PrivacyStatsData, rhs: PrivacyStatsData) -> Bool { - lhs.totalCount == rhs.totalCount && Set(lhs.trackerCompanies) == Set(rhs.trackerCompanies) - } - } - - struct TrackerCompany: Encodable, Equatable, Hashable { - let count: Int64 - let displayName: String - - static func otherCompanies(count: Int64) -> TrackerCompany { - TrackerCompany(count: count, displayName: "__other__") - } - } -} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift index d2aee1d6f5..56a6cdff80 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/PrivacyStats/NewTabPagePrivacyStatsModel.swift @@ -129,25 +129,25 @@ public final class NewTabPagePrivacyStatsModel { eventMapping?.fire(.showMore) } - func calculatePrivacyStats() async -> NewTabPagePrivacyStatsClient.PrivacyStatsData { + func calculatePrivacyStats() async -> NewTabPageDataModel.PrivacyStatsData { let stats = await privacyStats.fetchPrivacyStats() var totalCount: Int64 = 0 var otherCount: Int64 = 0 - var companiesStats: [NewTabPagePrivacyStatsClient.TrackerCompany] = stats.compactMap { key, value in + var companiesStats: [NewTabPageDataModel.TrackerCompany] = stats.compactMap { key, value in totalCount += value guard topCompanies.contains(key) else { otherCount += value return nil } - return NewTabPagePrivacyStatsClient.TrackerCompany(count: value, displayName: key) + return NewTabPageDataModel.TrackerCompany(count: value, displayName: key) } if otherCount > 0 { companiesStats.append(.otherCompanies(count: otherCount)) } - return NewTabPagePrivacyStatsClient.PrivacyStatsData(totalCount: totalCount, trackerCompanies: companiesStats) + return NewTabPageDataModel.PrivacyStatsData(totalCount: totalCount, trackerCompanies: companiesStats) } private func refreshTopCompanies() { From 14ea76dd91e6f7404bd99f6dbf8da37aa74a1ee1 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 17:27:22 +0100 Subject: [PATCH 36/62] Move RMF models to NewTabPageDataModel --- .../RMF/NewTabPageDataModel+RMF.swift | 138 ++++++++++++++++++ .../NewTabPage/RMF/NewTabPageRMFClient.swift | 132 +---------------- 2 files changed, 145 insertions(+), 125 deletions(-) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageDataModel+RMF.swift diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageDataModel+RMF.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageDataModel+RMF.swift new file mode 100644 index 0000000000..2d2ede6230 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageDataModel+RMF.swift @@ -0,0 +1,138 @@ +// +// NewTabPageDataModel+RMF.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 RemoteMessaging + +extension NewTabPageDataModel { + + struct RemoteMessageParams: Codable { + let id: String + } + + struct RMFData: Encodable { + let content: RMFMessage? + } + + enum RMFMessage: Encodable, Equatable { + 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 + } + } + + init?(_ remoteMessageModel: RemoteMessageModel) { + guard let modelType = remoteMessageModel.content else { + return nil + } + + switch modelType { + case let .small(titleText, descriptionText): + self = .small(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText)) + + case let .medium(titleText, descriptionText, placeholder): + self = .medium(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText, icon: .init(placeholder))) + + case let .bigSingleAction(titleText, descriptionText, placeholder, primaryActionText, _): + self = .bigSingleAction(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText, icon: .init(placeholder), primaryActionText: primaryActionText)) + + case let .bigTwoAction(titleText, descriptionText, placeholder, primaryActionText, _, secondaryActionText, _): + self = .bigTwoAction(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText, icon: .init(placeholder), primaryActionText: primaryActionText, secondaryActionText: secondaryActionText)) + + default: + return nil + } + } + } + + struct SmallMessage: Encodable, Equatable { + let messageType = "small" + + let id: String + let titleText: String + let descriptionText: String + } + + struct MediumMessage: Encodable, Equatable { + let messageType = "medium" + + let id: String + let titleText: String + let descriptionText: String + let icon: RMFIcon + } + + struct BigSingleActionMessage: Encodable, Equatable { + let messageType = "big_single_action" + + let id: String + let titleText: String + let descriptionText: String + let icon: RMFIcon + let primaryActionText: String + } + + struct BigTwoActionMessage: Encodable, Equatable { + let messageType = "big_two_action" + + let id: String + let titleText: String + let descriptionText: String + let icon: RMFIcon + let primaryActionText: String + let secondaryActionText: String + } + + enum RMFIcon: String, Encodable { + case announce = "Announce" + case ddgAnnounce = "DDGAnnounce" + case criticalUpdate = "CriticalUpdate" + case appUpdate = "AppUpdate" + case privacyPro = "PrivacyPro" + + init(_ placeholder: RemotePlaceholder) { + switch placeholder { + case .announce: + self = .announce + case .ddgAnnounce: + self = .ddgAnnounce + case .criticalUpdate: + self = .criticalUpdate + case .appUpdate: + self = .appUpdate + case .privacyShield: + self = .privacyPro + default: + self = .ddgAnnounce + } + } + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift index bb8d38a9ac..803a2445d9 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/RMF/NewTabPageRMFClient.swift @@ -63,14 +63,14 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { @MainActor private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let remoteMessage = remoteMessageProvider.remoteMessage else { - return NewTabPageUserScript.RMFData(content: nil) + return NewTabPageDataModel.RMFData(content: nil) } - return NewTabPageUserScript.RMFData(content: .init(remoteMessage)) + return NewTabPageDataModel.RMFData(content: .init(remoteMessage)) } private func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let remoteMessageParams: NewTabPageUserScript.RemoteMessageParams = DecodableHelper.decode(from: params), + guard let remoteMessageParams: NewTabPageDataModel.RemoteMessageParams = DecodableHelper.decode(from: params), remoteMessageParams.id == remoteMessageProvider.remoteMessage?.id else { return nil @@ -81,7 +81,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { } private func primaryAction(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let remoteMessageParams: NewTabPageUserScript.RemoteMessageParams = DecodableHelper.decode(from: params), + guard let remoteMessageParams: NewTabPageDataModel.RemoteMessageParams = DecodableHelper.decode(from: params), remoteMessageParams.id == remoteMessageProvider.remoteMessage?.id else { return nil @@ -99,7 +99,7 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { } private func secondaryAction(params: Any, original: WKScriptMessage) async throws -> Encodable? { - guard let remoteMessageParams: NewTabPageUserScript.RemoteMessageParams = DecodableHelper.decode(from: params), + guard let remoteMessageParams: NewTabPageDataModel.RemoteMessageParams = DecodableHelper.decode(from: params), remoteMessageParams.id == remoteMessageProvider.remoteMessage?.id else { return nil @@ -115,131 +115,13 @@ public final class NewTabPageRMFClient: NewTabPageScriptClient { } private func notifyRemoteMessageDidChange(_ remoteMessage: RemoteMessageModel?) { - let data: NewTabPageUserScript.RMFData = { + let data: NewTabPageDataModel.RMFData = { guard let remoteMessage, remoteMessageProvider.isMessageSupported(remoteMessage) else { return .init(content: nil) } - return .init(content: NewTabPageUserScript.RMFMessage(remoteMessage)) + return .init(content: NewTabPageDataModel.RMFMessage(remoteMessage)) }() pushMessage(named: MessageName.rmfOnDataUpdate.rawValue, params: data) } } - -extension NewTabPageUserScript { - - struct RemoteMessageParams: Codable { - let id: String - } - - struct RMFData: Encodable { - let content: RMFMessage? - } - - enum RMFMessage: Encodable, Equatable { - 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 - } - } - - init?(_ remoteMessageModel: RemoteMessageModel) { - guard let modelType = remoteMessageModel.content else { - return nil - } - - switch modelType { - case let .small(titleText, descriptionText): - self = .small(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText)) - - case let .medium(titleText, descriptionText, placeholder): - self = .medium(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText, icon: .init(placeholder))) - - case let .bigSingleAction(titleText, descriptionText, placeholder, primaryActionText, _): - self = .bigSingleAction(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText, icon: .init(placeholder), primaryActionText: primaryActionText)) - - case let .bigTwoAction(titleText, descriptionText, placeholder, primaryActionText, _, secondaryActionText, _): - self = .bigTwoAction(.init(id: remoteMessageModel.id, titleText: titleText, descriptionText: descriptionText, icon: .init(placeholder), primaryActionText: primaryActionText, secondaryActionText: secondaryActionText)) - - default: - return nil - } - } - } - - struct SmallMessage: Encodable, Equatable { - let messageType = "small" - - let id: String - let titleText: String - let descriptionText: String - } - - struct MediumMessage: Encodable, Equatable { - let messageType = "medium" - - let id: String - let titleText: String - let descriptionText: String - let icon: RMFIcon - } - - struct BigSingleActionMessage: Encodable, Equatable { - let messageType = "big_single_action" - - let id: String - let titleText: String - let descriptionText: String - let icon: RMFIcon - let primaryActionText: String - } - - struct BigTwoActionMessage: Encodable, Equatable { - let messageType = "big_two_action" - - let id: String - let titleText: String - let descriptionText: String - let icon: RMFIcon - let primaryActionText: String - let secondaryActionText: String - } - - enum RMFIcon: String, Encodable { - case announce = "Announce" - case ddgAnnounce = "DDGAnnounce" - case criticalUpdate = "CriticalUpdate" - case appUpdate = "AppUpdate" - case privacyPro = "PrivacyPro" - - init(_ placeholder: RemotePlaceholder) { - switch placeholder { - case .announce: - self = .announce - case .ddgAnnounce: - self = .ddgAnnounce - case .criticalUpdate: - self = .criticalUpdate - case .appUpdate: - self = .appUpdate - case .privacyShield: - self = .privacyPro - default: - self = .ddgAnnounce - } - } - } -} From 9d3b8c017359890a0850471ffd72b41b370518a9 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 18:34:06 +0100 Subject: [PATCH 37/62] Update unit tests code --- ...ngNewTabPageCustomBackgroundProvider.swift | 54 +++++++++++++++++++ ...ringNewTabPageNextStepsCardsProvider.swift | 18 +++---- .../NewTabPageConfigurationClientTests.swift | 11 ++-- .../NewTabPageFavoritesClientTests.swift | 16 +++--- .../NewTabPageNextStepsCardsClientTests.swift | 16 +++--- .../NewTabPagePrivacyStatsClientTests.swift | 4 +- .../NewTabPagePrivacyStatsModelTests.swift | 4 +- .../NewTabPageRMFClientTests.swift | 30 +++++------ ...ewTabPageNextStepsCardsProviderTests.swift | 6 +-- 9 files changed, 107 insertions(+), 52 deletions(-) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift new file mode 100644 index 0000000000..78ed98e6c7 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift @@ -0,0 +1,54 @@ +// +// CapturingNewTabPageCustomBackgroundProvider.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 NewTabPage + +final class CapturingNewTabPageCustomBackgroundProvider: NewTabPageCustomBackgroundProviding { + var customizerData: NewTabPageDataModel.CustomizerData = .init(background: .default, theme: .none, userImages: []) + + @Published + var background: NewTabPageDataModel.Background = .default + + var backgroundPublisher: AnyPublisher { + $background.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + @Published + var theme: NewTabPageDataModel.Theme? + + var themePublisher: AnyPublisher { + $theme.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + @Published + var userImages: [NewTabPageDataModel.UserImage] = [] + + var userImagesPublisher: AnyPublisher<[NewTabPageDataModel.UserImage], Never> { + $userImages.dropFirst().removeDuplicates().eraseToAnyPublisher() + } + + func presentUploadDialog() async { + } + + func deleteImage(with imageID: String) async { + } + + var presentUploadDialogCallsCount: Int = 0 + var deleteImageCalls: [String] = [] +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift index 4cc61f4c50..2df71817cc 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageNextStepsCardsProvider.swift @@ -27,26 +27,26 @@ final class CapturingNewTabPageNextStepsCardsProvider: NewTabPageNextStepsCardsP $isViewExpanded.dropFirst().removeDuplicates().eraseToAnyPublisher() } - @Published var cards: [NewTabPageNextStepsCardsClient.CardID] = [] - var cardsPublisher: AnyPublisher<[NewTabPageNextStepsCardsClient.CardID], Never> { + @Published var cards: [NewTabPageDataModel.CardID] = [] + var cardsPublisher: AnyPublisher<[NewTabPageDataModel.CardID], Never> { $cards.dropFirst().removeDuplicates().eraseToAnyPublisher() } - func handleAction(for card: NewTabPageNextStepsCardsClient.CardID) { + func handleAction(for card: NewTabPageDataModel.CardID) { handleActionCalls.append(card) } - func dismiss(_ card: NewTabPageNextStepsCardsClient.CardID) { + func dismiss(_ card: NewTabPageDataModel.CardID) { dismissCalls.append(card) } - func willDisplayCards(_ cards: [NewTabPageNextStepsCardsClient.CardID]) { + func willDisplayCards(_ cards: [NewTabPageDataModel.CardID]) { willDisplayCardsCalls.append(cards) willDisplayCardsImpl?(cards) } - var handleActionCalls: [NewTabPageNextStepsCardsClient.CardID] = [] - var dismissCalls: [NewTabPageNextStepsCardsClient.CardID] = [] - var willDisplayCardsCalls: [[NewTabPageNextStepsCardsClient.CardID]] = [] - var willDisplayCardsImpl: (([NewTabPageNextStepsCardsClient.CardID]) -> Void)? + var handleActionCalls: [NewTabPageDataModel.CardID] = [] + var dismissCalls: [NewTabPageDataModel.CardID] = [] + var willDisplayCardsCalls: [[NewTabPageDataModel.CardID]] = [] + var willDisplayCardsImpl: (([NewTabPageDataModel.CardID]) -> Void)? } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift index 8f5edf319b..b4d9649748 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift @@ -33,6 +33,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { contextMenuPresenter = CapturingNewTabPageContextMenuPresenter() client = NewTabPageConfigurationClient( sectionsVisibilityProvider: sectionsVisibilityProvider, + customBackgroundProvider: CapturingNewTabPageCustomBackgroundProvider(), contextMenuPresenter: contextMenuPresenter ) @@ -47,7 +48,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { sectionsVisibilityProvider.isFavoritesVisible = true sectionsVisibilityProvider.isPrivacyStatsVisible = false - let parameters = NewTabPageUserScript.ContextMenuParams(visibilityMenuItems: [ + let parameters = NewTabPageDataModel.ContextMenuParams(visibilityMenuItems: [ .init(id: .favorites, title: "Favorites"), .init(id: .privacyStats, title: "Privacy Stats") ]) @@ -63,7 +64,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { } func testWhenContextMenuParamsIsEmptyThenContextMenuDoesNotShow() async throws { - let parameters = NewTabPageUserScript.ContextMenuParams(visibilityMenuItems: []) + let parameters = NewTabPageDataModel.ContextMenuParams(visibilityMenuItems: []) try await sendMessageExpectingNilResponse(named: .contextMenu, parameters: parameters) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 0) @@ -72,7 +73,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { // MARK: - initialSetup func testThatInitialSetupReturnsConfiguration() async throws { - let configuration: NewTabPageUserScript.NewTabPageConfiguration = try await sendMessage(named: .initialSetup) + let configuration: NewTabPageDataModel.NewTabPageConfiguration = try await sendMessage(named: .initialSetup) XCTAssertEqual(configuration.widgets, [ .init(id: .rmf), .init(id: .nextSteps), @@ -89,7 +90,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { // MARK: - widgetsSetConfig func testWhenWidgetsSetConfigIsReceivedThenWidgetConfigsAreUpdated() async throws { - let configs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ + let configs: [NewTabPageDataModel.NewTabPageConfiguration.WidgetConfig] = [ .init(id: .favorites, isVisible: false), .init(id: .privacyStats, isVisible: true) ] @@ -101,7 +102,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { func testWhenWidgetsSetConfigIsReceivedWithPartialConfigThenOnlyIncludedWidgetsConfigsAreUpdated() async throws { let initialIsFavoritesVisible = sectionsVisibilityProvider.isFavoritesVisible - let configs: [NewTabPageUserScript.NewTabPageConfiguration.WidgetConfig] = [ + let configs: [NewTabPageDataModel.NewTabPageConfiguration.WidgetConfig] = [ .init(id: .privacyStats, isVisible: false) ] try await sendMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index d3cdba7267..73024515dc 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -98,7 +98,7 @@ final class NewTabPageFavoritesClientTests: XCTestCase { MockNewTabPageFavorite(id: "2", title: "D", url: "https://d.com"), MockNewTabPageFavorite(id: "3", title: "E", url: "https://e.com") ] - let data: NewTabPageFavoritesClientUnderTest.FavoritesData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.FavoritesData = try await handleMessage(named: .getData) XCTAssertEqual(data.favorites, [ .init(id: "1", title: "A", url: "https://a.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//a.com")), .init(id: "10", title: "B", url: "https://b.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//b.com")), @@ -110,20 +110,20 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testWhenFavoritesAreEmptyThenGetDataReturnsNoFavorites() async throws { favoritesModel.favorites = [] - let data: NewTabPageFavoritesClientUnderTest.FavoritesData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.FavoritesData = try await handleMessage(named: .getData) XCTAssertEqual(data.favorites, []) } // MARK: - move func testThatMoveActionIsForwardedToTheModel() async throws { - let action = NewTabPageFavoritesClientUnderTest.FavoritesMoveAction(id: "abcd", fromIndex: 10, targetIndex: 4) + let action = NewTabPageDataModel.FavoritesMoveAction(id: "abcd", fromIndex: 10, targetIndex: 4) try await handleMessageExpectingNilResponse(named: .move, parameters: action) XCTAssertEqual(actionsHandler.moveCalls, [.init("abcd", 4)]) } func testThatWhenFavoriteIsMovedToHigherIndexThenModelIncrementsIndex() async throws { - let action = NewTabPageFavoritesClientUnderTest.FavoritesMoveAction(id: "abcd", fromIndex: 1, targetIndex: 4) + let action = NewTabPageDataModel.FavoritesMoveAction(id: "abcd", fromIndex: 1, targetIndex: 4) try await handleMessageExpectingNilResponse(named: .move, parameters: action) XCTAssertEqual(actionsHandler.moveCalls, [.init("abcd", 5)]) } @@ -131,13 +131,13 @@ final class NewTabPageFavoritesClientTests: XCTestCase { // MARK: - open func testThatOpenActionIsForwardedToTheModel() async throws { - let action = NewTabPageFavoritesClientUnderTest.FavoritesOpenAction(id: "abcd", url: "https://example.com") + let action = NewTabPageDataModel.FavoritesOpenAction(id: "abcd", url: "https://example.com") try await handleMessageExpectingNilResponse(named: .open, parameters: action) XCTAssertEqual(actionsHandler.openCalls, [.init(URL(string: "https://example.com")!, .current)]) } func testWhenURLIsInvalidThenOpenActionIsNotForwardedToTheModel() async throws { - let action = NewTabPageFavoritesClientUnderTest.FavoritesOpenAction(id: "abcd", url: "abcd") + let action = NewTabPageDataModel.FavoritesOpenAction(id: "abcd", url: "abcd") try await handleMessageExpectingNilResponse(named: .open, parameters: action) XCTAssertEqual(actionsHandler.openCalls, []) } @@ -146,14 +146,14 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testThatOpenContextMenuActionForExistingFavoriteIsForwardedToTheModel() async throws { favoritesModel.favorites = [.init(id: "abcd", title: "A", url: "https://example.com")] - let action = NewTabPageFavoritesClientUnderTest.FavoritesContextMenuAction(id: "abcd") + let action = NewTabPageDataModel.FavoritesContextMenuAction(id: "abcd") try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 1) } func testThatOpenContextMenuActionForNotExistingFavoriteIsNotForwardedToTheModel() async throws { favoritesModel.favorites = [] - let action = NewTabPageFavoritesClientUnderTest.FavoritesContextMenuAction(id: "abcd") + let action = NewTabPageDataModel.FavoritesContextMenuAction(id: "abcd") try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 0) } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift index 82acec3fc9..df4cf389d0 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift @@ -39,18 +39,18 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { // MARK: - action func testThatActionCallsHandleAction() async throws { - try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageNextStepsCardsClient.Card(id: .defaultApp)) - try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageNextStepsCardsClient.Card(id: .duckplayer)) - try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageNextStepsCardsClient.Card(id: .bringStuff)) + try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .defaultApp)) + try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .duckplayer)) + try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .bringStuff)) XCTAssertEqual(model.handleActionCalls, [.defaultApp, .duckplayer, .bringStuff]) } // MARK: - dismiss func testThatDismissCallsDismissHandler() async throws { - try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageNextStepsCardsClient.Card(id: .defaultApp)) - try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageNextStepsCardsClient.Card(id: .duckplayer)) - try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageNextStepsCardsClient.Card(id: .bringStuff)) + try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .defaultApp)) + try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .duckplayer)) + try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .bringStuff)) XCTAssertEqual(model.dismissCalls, [.defaultApp, .duckplayer, .bringStuff]) } @@ -94,7 +94,7 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { .duckplayer, .bringStuff ] - let data: NewTabPageNextStepsCardsClient.NextStepsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.NextStepsData = try await handleMessage(named: .getData) XCTAssertEqual(data, .init(content: [ .init(id: .addAppToDockMac), .init(id: .duckplayer), @@ -104,7 +104,7 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { func testWhenCardsAreEmptyThenGetDataReturnsNilContent() async throws { model.cards = [] - let data: NewTabPageNextStepsCardsClient.NextStepsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.NextStepsData = try await handleMessage(named: .getData) XCTAssertEqual(data, .init(content: nil)) } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift index 3202f76922..ac4db71db0 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift @@ -112,7 +112,7 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { userScript = NewTabPageUserScript() client.registerMessageHandlers(for: userScript) - let data: NewTabPagePrivacyStatsClient.PrivacyStatsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.PrivacyStatsData = try await handleMessage(named: .getData) XCTAssertEqual(data, .init(totalCount: 2510, trackerCompanies: [ .init(count: 1, displayName: "A"), .init(count: 2, displayName: "B"), @@ -123,7 +123,7 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { } func testWhenPrivacyStatsAreEmptyThenGetDataReturnsEmptyArray() async throws { - let data: NewTabPagePrivacyStatsClient.PrivacyStatsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.PrivacyStatsData = try await handleMessage(named: .getData) XCTAssertEqual(data, .init(totalCount: 0, trackerCompanies: [])) } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift index fc4d46e4db..6db5bbe685 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift @@ -113,7 +113,7 @@ final class NewTabPagePrivacyStatsModelTests: XCTestCase { privacyStats.privacyStats = ["A": 1, "B": 2, "C": 3, "D": 4, "E": 1500, "F": 100, "G": 900] - let stats: NewTabPagePrivacyStatsClient.PrivacyStatsData = await model.calculatePrivacyStats() + let stats: NewTabPageDataModel.PrivacyStatsData = await model.calculatePrivacyStats() XCTAssertEqual(stats, .init(totalCount: 2510, trackerCompanies: [ .init(count: 1, displayName: "A"), @@ -143,7 +143,7 @@ final class NewTabPagePrivacyStatsModelTests: XCTestCase { privacyStats.privacyStats = ["A": 1, "B": 2, "C": 3, "D": 4, "E": 1500, "F": 100, "G": 900] - let stats: NewTabPagePrivacyStatsClient.PrivacyStatsData = await model.calculatePrivacyStats() + let stats: NewTabPageDataModel.PrivacyStatsData = await model.calculatePrivacyStats() XCTAssertEqual(stats, .init(totalCount: 2510, trackerCompanies: [ .init(count: 1, displayName: "A"), diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift index ae271591b3..b29247ec30 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift @@ -35,7 +35,7 @@ final class NewTabPageRMFClientTests: XCTestCase { } func testWhenMessageIsNilThenGetDataReturnsNilMessage() async throws { - let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) XCTAssertNil(rmfData.content) } @@ -43,21 +43,21 @@ final class NewTabPageRMFClientTests: XCTestCase { func testThatGetDataReturnsSmallMessageIfPresent() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .small(.init(id: "sample_message", titleText: "title", descriptionText: "description"))) } func testThatGetDataReturnsMediumMessageIfPresent() async throws { remoteMessageProvider.remoteMessage = .mockMedium(id: "sample_message") - let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .medium(.init(id: "sample_message", titleText: "title", descriptionText: "description", icon: .criticalUpdate))) } func testThatGetDataReturnsBigSingleActionMessageIfPresent() async throws { remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) - let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .bigSingleAction( .init( @@ -72,7 +72,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testThatGetDataReturnsBigTwoActionMessageIfPresent() async throws { remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) - let rmfData: NewTabPageUserScript.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .bigTwoAction( .init( @@ -91,7 +91,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testThatDismissSendsDismissActionToProvider() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: nil, button: .close)]) } @@ -99,7 +99,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenMessageIdDoesNotMatchThenDismissHasNoEffect() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) XCTAssertTrue(remoteMessageProvider.dismissCalls.isEmpty) } @@ -109,7 +109,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenSingleActionMessageThenPrimaryActionSendsActionToProvider() async throws { remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .action)]) } @@ -117,7 +117,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenTwoActionMessageThenPrimaryActionSendsPrimaryActionToProvider() async throws { remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .primaryAction)]) } @@ -125,7 +125,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenMessageHasNoButtonThenPrimaryActionHasNoEffect() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -133,7 +133,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenMessageIdDoesNotMatchThenPrimaryActionHasNoEffect() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -143,7 +143,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenTwoActionMessageThenSecondaryActionSendsSecondaryActionToProvider() async throws { remoteMessageProvider.remoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .dismiss, secondaryAction: .appStore) - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .secondaryAction)]) } @@ -151,7 +151,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenSingleActionMessageThenSecondaryActionHasNoEffect() async throws { remoteMessageProvider.remoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -159,7 +159,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenMessageHasNoButtonThenSecondaryActionHasNoEffect() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -167,7 +167,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testWhenMessageIdDoesNotMatchThenSecondaryActionHasNoEffect() async throws { remoteMessageProvider.remoteMessage = .mockSmall(id: "sample_message") - let parameters = NewTabPageUserScript.RemoteMessageParams(id: "different_sample_message") + let parameters = NewTabPageDataModel.RemoteMessageParams(id: "different_sample_message") try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } diff --git a/UnitTests/NewTabPage/NewTabPageNextStepsCardsProviderTests.swift b/UnitTests/NewTabPage/NewTabPageNextStepsCardsProviderTests.swift index 4b9f30a79a..6cfd7ca364 100644 --- a/UnitTests/NewTabPage/NewTabPageNextStepsCardsProviderTests.swift +++ b/UnitTests/NewTabPage/NewTabPageNextStepsCardsProviderTests.swift @@ -65,7 +65,7 @@ final class NewTabPageNextStepsCardsProviderTests: XCTestCase { provider.appearancePreferences.isContinueSetUpCardsViewOutdated = false provider.continueSetUpModel.featuresMatrix = [[.defaultBrowser]] - var cardsEvents = [[NewTabPageNextStepsCardsClient.CardID]]() + var cardsEvents = [[NewTabPageDataModel.CardID]]() let cancellable = provider.cardsPublisher .sink { cards in @@ -84,7 +84,7 @@ final class NewTabPageNextStepsCardsProviderTests: XCTestCase { provider.appearancePreferences.isContinueSetUpCardsViewOutdated = true provider.continueSetUpModel.featuresMatrix = [[.defaultBrowser]] - var cardsEvents = [[NewTabPageNextStepsCardsClient.CardID]]() + var cardsEvents = [[NewTabPageDataModel.CardID]]() let cancellable = provider.cardsPublisher .sink { cards in @@ -103,7 +103,7 @@ final class NewTabPageNextStepsCardsProviderTests: XCTestCase { provider.appearancePreferences.isContinueSetUpCardsViewOutdated = false provider.continueSetUpModel.featuresMatrix = [[.defaultBrowser]] - var cardsEvents = [[NewTabPageNextStepsCardsClient.CardID]]() + var cardsEvents = [[NewTabPageDataModel.CardID]]() let cancellable = provider.cardsPublisher .sink { cards in From ac5bdda20379091e5905738ad64a3faed44358a6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 12 Dec 2024 23:54:36 +0100 Subject: [PATCH 38/62] Add tests for NewTabPageCustomBackgroundClient --- ...ngNewTabPageCustomBackgroundProvider.swift | 2 + ...ewTabPageCustomBackgroundClientTests.swift | 87 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift index 78ed98e6c7..4a964d950a 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift @@ -44,9 +44,11 @@ final class CapturingNewTabPageCustomBackgroundProvider: NewTabPageCustomBackgro } func presentUploadDialog() async { + presentUploadDialogCallsCount += 1 } func deleteImage(with imageID: String) async { + deleteImageCalls.append(imageID) } var presentUploadDialogCallsCount: Int = 0 diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift new file mode 100644 index 0000000000..38a3e3a675 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift @@ -0,0 +1,87 @@ +// +// NewTabPageCustomBackgroundClientTests.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 +import Combine +import XCTest +@testable import NewTabPage + +final class NewTabPageCustomBackgroundClientTests: XCTestCase { + var client: NewTabPageCustomBackgroundClient! + var model: CapturingNewTabPageCustomBackgroundProvider! + var userScript: NewTabPageUserScript! + + override func setUpWithError() throws { + try super.setUpWithError() + model = CapturingNewTabPageCustomBackgroundProvider() + client = NewTabPageCustomBackgroundClient(model: model) + + userScript = NewTabPageUserScript() + client.registerMessageHandlers(for: userScript) + } + + // MARK: - deleteImage + + func testThatDeleteImageCallsModel() async throws { + let deleteData = NewTabPageDataModel.DeleteImageData(id: "abcd") + try await handleMessageExpectingNilResponse(named: .deleteImage, parameters: deleteData) + XCTAssertEqual(model.deleteImageCalls, ["abcd"]) + } + + // MARK: - setBackground + + func testThatSetBackgroundCallsModel() async throws { + let backgroundData = NewTabPageDataModel.BackgroundData(background: .gradient("gradient01")) + try await handleMessageExpectingNilResponse(named: .setBackground, parameters: backgroundData) + XCTAssertEqual(model.background, .gradient("gradient01")) + } + + // MARK: - setTheme + + func testThatSetThemeCallsModel() async throws { + let themeData = NewTabPageDataModel.ThemeData(theme: .dark) + try await handleMessageExpectingNilResponse(named: .setTheme, parameters: themeData) + XCTAssertEqual(model.theme, .dark) + } + + // MARK: - upload + + func testThatUploadCallsModel() async throws { + try await handleMessageExpectingNilResponse(named: .upload) + XCTAssertEqual(model.presentUploadDialogCallsCount, 1) + } + + // MARK: - Helper functions + + func handleMessage(named methodName: NewTabPageCustomBackgroundClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + return try XCTUnwrap(response as? Response, file: file, line: line) + } + + func handleMessageIgnoringResponse(named methodName: NewTabPageCustomBackgroundClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + } + + func handleMessageExpectingNilResponse(named methodName: NewTabPageCustomBackgroundClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + XCTAssertNil(response, file: file, line: line) + } +} From f9e35ea93687b123f0c6fee4a79d892dc6bf616e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 13 Dec 2024 00:02:51 +0100 Subject: [PATCH 39/62] Add Background.Kind enum --- ...NewTabPageDataModel+CustomBackground.swift | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift index 34abf553f3..40e97e319e 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift @@ -84,32 +84,36 @@ public extension NewTabPageDataModel { case value } - var kind: String { + enum Kind: String, Codable { + case `default`, color, hex, gradient, userImage + } + + var kind: Kind { switch self { case .default: - return "default" + return .default case .solidColor: - return "color" + return .color case .hexColor: - return "hex" + return .hex case .gradient: - return "gradient" + return .gradient case .userImage: - return "userImage" + return .userImage } } public init(from decoder: any Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) - let kind = try container.decode(String.self, forKey: CodingKeys.kind) + let kind = try container.decode(Kind.self, forKey: CodingKeys.kind) switch kind { - case "color", "hex": + case .color, .hex: let value = try container.decode(String.self, forKey: CodingKeys.value) self = .solidColor(value) - case "gradient": + case .gradient: let value = try container.decode(String.self, forKey: CodingKeys.value) self = .gradient(value) - case "userImage": + case .userImage: let value = try container.decode(UserImage.self, forKey: CodingKeys.value) self = .userImage(value) default: From ea5769bfa8304e0dbaabd624f8a820158d4dcee9 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 13 Dec 2024 00:23:28 +0100 Subject: [PATCH 40/62] Pass last picked color to the NTP --- .../Model/HomePageSettings/HomePageSettingsModel.swift | 10 ++++++---- .../NewTabPage/NewTabPageCustomizationProvider.swift | 1 + .../NewTabPageDataModel+CustomBackground.swift | 9 ++++++++- .../CapturingNewTabPageCustomBackgroundProvider.swift | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift index cdec35634c..efbb51fb4b 100644 --- a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift @@ -244,8 +244,13 @@ extension HomePage.Models { @Published var customBackground: CustomBackground? { didSet { appearancePreferences.homePageCustomBackground = customBackground - if case .userImage(let userBackgroundImage) = customBackground { + switch customBackground { + case .solidColor(let solidColorBackground) where solidColorBackground.predefinedColorName == nil: + lastPickedCustomColor = solidColorBackground.color + case .userImage(let userBackgroundImage): customImagesManager?.updateSelectedTimestamp(for: userBackgroundImage) + default: + break } if let customBackground { Logger.homePageSettings.debug("Home page background updated: \(customBackground), color scheme: \(customBackground.colorScheme)") @@ -298,9 +303,6 @@ extension HomePage.Models { provider.showColorPanel(with: lastPickedCustomColorHexValue.flatMap(NSColor.init(hex:)) ?? Const.defaultColorPickerColor) userColorCancellable = provider.colorPublisher - .handleEvents(receiveOutput: { [weak self] color in - self?.lastPickedCustomColor = color - }) .map { CustomBackground.solidColor(.init(color: $0)) } .assign(to: \.customBackground, onWeaklyHeld: self) } diff --git a/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift b/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift index 8948316722..e755bdeec0 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift @@ -33,6 +33,7 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding .init( background: .init(homePageSettingsModel.customBackground), theme: .init(appearancePreferences.currentThemeName), + userColor: homePageSettingsModel.lastPickedCustomColor, userImages: homePageSettingsModel.availableUserBackgroundImages.map(NewTabPageDataModel.UserImage.init) ) } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift index 40e97e319e..fdc29c336b 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift @@ -16,6 +16,7 @@ // limitations under the License. // +import AppKitExtensions import Foundation public extension NewTabPageDataModel { @@ -23,12 +24,18 @@ public extension NewTabPageDataModel { struct CustomizerData: Encodable, Equatable { public let background: Background public let theme: Theme? + public let userColor: Background? public let userImages: [UserImage] - public init(background: Background, theme: Theme?, userImages: [UserImage]) { + public init(background: Background, theme: Theme?, userColor: NSColor?, userImages: [UserImage]) { self.background = background self.theme = theme self.userImages = userImages + if let hex = userColor?.hex() { + self.userColor = Background.hexColor(hex) + } else { + self.userColor = nil + } } enum CodingKeys: CodingKey { diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift index 4a964d950a..d463aef60e 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift @@ -20,7 +20,7 @@ import Combine import NewTabPage final class CapturingNewTabPageCustomBackgroundProvider: NewTabPageCustomBackgroundProviding { - var customizerData: NewTabPageDataModel.CustomizerData = .init(background: .default, theme: .none, userImages: []) + var customizerData: NewTabPageDataModel.CustomizerData = .init(background: .default, theme: .none, userColor: nil, userImages: []) @Published var background: NewTabPageDataModel.Background = .default From 6e3303ecd059116e53228671d0a6567ccf989882 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 13 Dec 2024 16:37:17 +0100 Subject: [PATCH 41/62] Actually send the color to NTP --- .../CustomBackground/NewTabPageDataModel+CustomBackground.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift index fdc29c336b..a619f1e7c9 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift @@ -41,6 +41,7 @@ public extension NewTabPageDataModel { enum CodingKeys: CodingKey { case background case theme + case userColor case userImages } @@ -48,6 +49,7 @@ public extension NewTabPageDataModel { var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.background, forKey: CodingKeys.background) try container.encode(self.theme?.rawValue ?? "system", forKey: CodingKeys.theme) + try container.encode(self.userColor, forKey: CodingKeys.userColor) try container.encode(self.userImages, forKey: CodingKeys.userImages) } } From 40cc65fe80f4ab1ce589a1618843e8a8ac7a6890 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 18 Dec 2024 14:59:52 +0100 Subject: [PATCH 42/62] Update maximumNumberOfUserImages to 8 --- .../HomePage/Model/HomePageSettings/HomePageSettingsModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift index efbb51fb4b..a693074e8a 100644 --- a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift @@ -59,7 +59,7 @@ extension HomePage.Models { final class SettingsModel: ObservableObject { enum Const { - static let maximumNumberOfUserImages = 4 + static let maximumNumberOfUserImages = 8 static let defaultColorPickerColor = NSColor.white } From 74f37a42652759045dd456e82f50b360bfb50dc6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 18 Dec 2024 15:49:55 +0100 Subject: [PATCH 43/62] Add link opener to NewTabPageConfigurationClient --- DuckDuckGo.xcodeproj/project.pbxproj | 6 ++++ ...ModelNavigator+NewTabPageLinkOpening.swift | 29 +++++++++++++++++++ .../NewTabPageActionsManagerExtension.swift | 6 +++- .../NewTabPageConfigurationClient.swift | 19 +++++++++++- .../NewTabPageDataModel+Configuration.swift | 10 +++++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d1f2c8eeb7..6e47cf81b4 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1308,6 +1308,8 @@ 37F44A5F298C17830025E7FE /* Navigation in Frameworks */ = {isa = PBXBuildFile; productRef = 37F44A5E298C17830025E7FE /* Navigation */; }; 37F8ABD32CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; 37F8ABD42CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */; }; + 37F9AEB12D131705007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9AEB02D1316FE007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift */; }; + 37F9AEB22D131705007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9AEB02D1316FE007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift */; }; 37FC2A182CF903080048E226 /* MockPrivacyStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */; }; 37FC2A192CF903080048E226 /* MockPrivacyStats.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */; }; 37FD78112A29EBD100B36DB1 /* SyncErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD78102A29EBD100B36DB1 /* SyncErrorHandler.swift */; }; @@ -3788,6 +3790,7 @@ 37F19A6628E1B43200740DC6 /* DuckPlayerPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayerPreferences.swift; sourceTree = ""; }; 37F19A6928E2F2D000740DC6 /* DuckPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DuckPlayer.swift; sourceTree = ""; }; 37F8ABD22CE3EE5B00CB0294 /* FeatureFlagOverridesMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeatureFlagOverridesMenu.swift; sourceTree = ""; }; + 37F9AEB02D1316FE007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift"; sourceTree = ""; }; 37FC2A172CF903060048E226 /* MockPrivacyStats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrivacyStats.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 = ""; }; @@ -5837,6 +5840,7 @@ 376788092CECCAD900F59D83 /* NewTabPage */ = { isa = PBXGroup; children = ( + 37F9AEB02D1316FE007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift */, 371E1D662D0B2E9F00F9205B /* NewTabPageCustomizationProvider.swift */, 37BE08792D09C0EA00C77B8E /* NewTabPageNextStepsCardsProvider.swift */, 37E307B12D075B5E00599500 /* NewTabPagePrivacyStatsEventHandler.swift */, @@ -12144,6 +12148,7 @@ F1FD5B682C2B0AAA0040FA0D /* SubscriptionPagesUseSubscriptionFeature.swift in Sources */, 3706FC82293F65D500E42796 /* PermissionAuthorizationPopover.swift in Sources */, 4BCBE4552BA7E16600FC75A1 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, + 37F9AEB22D131705007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift in Sources */, 371209312C233D69003ADF3D /* RemoteMessagingStoreErrorHandling.swift in Sources */, 3706FC83293F65D500E42796 /* PopoverMessageViewController.swift in Sources */, 7B5A23702C46A116007213AC /* ExcludedDomainsModel.swift in Sources */, @@ -13686,6 +13691,7 @@ 9F9C49F92BC7BC970099738D /* BookmarkAllTabsDialogView.swift in Sources */, B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */, AAA0CC472533833C0079BC96 /* MoreOptionsMenu.swift in Sources */, + 37F9AEB12D131705007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift in Sources */, B64C84E32692DC9F0048FEBE /* PermissionAuthorizationViewController.swift in Sources */, 4B92929D26670D2A00AD2C21 /* BookmarkNode.swift in Sources */, 1DEDB3642C19934C006B6D1B /* MoreOptionsMenuButton.swift in Sources */, diff --git a/DuckDuckGo/NewTabPage/DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift b/DuckDuckGo/NewTabPage/DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift new file mode 100644 index 0000000000..7bf52aab38 --- /dev/null +++ b/DuckDuckGo/NewTabPage/DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift @@ -0,0 +1,29 @@ +// +// DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.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 NewTabPage + +extension DefaultHomePageSettingsModelNavigator: NewTabPageLinkOpening { + + func openLink(_ target: NewTabPageDataModel.OpenAction.Target) async { + switch target { + case .settings: + openAppearanceSettings() + } + } +} diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index 6b4f03eea7..dcade5a53b 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -44,7 +44,11 @@ extension NewTabPageActionsManager { let customizationProvider = NewTabPageCustomizationProvider(homePageSettingsModel: NSApp.delegateTyped.homePageSettingsModel) self.init(scriptClients: [ - NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences, customBackgroundProvider: customizationProvider), + NewTabPageConfigurationClient( + sectionsVisibilityProvider: appearancePreferences, + customBackgroundProvider: customizationProvider, + linkOpener: DefaultHomePageSettingsModelNavigator() + ), NewTabPageCustomBackgroundClient(model: customizationProvider), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), NewTabPageNextStepsCardsClient(model: NewTabPageNextStepsCardsProvider(continueSetUpModel: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener()))), diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift index a85ebdfc09..166c590071 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift @@ -31,6 +31,10 @@ public protocol NewTabPageSectionsVisibilityProviding: AnyObject { var isPrivacyStatsVisiblePublisher: AnyPublisher { get } } +public protocol NewTabPageLinkOpening { + func openLink(_ target: NewTabPageDataModel.OpenAction.Target) async +} + public final class NewTabPageConfigurationClient: NewTabPageScriptClient { public weak var userScriptsSource: NewTabPageUserScriptsSource? @@ -39,15 +43,18 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { private let sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding private let customBackgroundProvider: NewTabPageCustomBackgroundProviding private let contextMenuPresenter: NewTabPageContextMenuPresenting + private let linkOpener: NewTabPageLinkOpening public init( sectionsVisibilityProvider: NewTabPageSectionsVisibilityProviding, customBackgroundProvider: NewTabPageCustomBackgroundProviding, - contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter() + contextMenuPresenter: NewTabPageContextMenuPresenting = DefaultNewTabPageContextMenuPresenter(), + linkOpener: NewTabPageLinkOpening ) { self.sectionsVisibilityProvider = sectionsVisibilityProvider self.customBackgroundProvider = customBackgroundProvider self.contextMenuPresenter = contextMenuPresenter + self.linkOpener = linkOpener Publishers.Merge(sectionsVisibilityProvider.isFavoritesVisiblePublisher, sectionsVisibilityProvider.isPrivacyStatsVisiblePublisher) .receive(on: DispatchQueue.main) @@ -60,6 +67,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { enum MessageName: String, CaseIterable { case contextMenu case initialSetup + case open case reportInitException case reportPageException case widgetsSetConfig = "widgets_setConfig" @@ -70,6 +78,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { userScript.registerMessageHandlers([ MessageName.contextMenu.rawValue: { [weak self] in try await self?.showContextMenu(params: $0, original: $1) }, MessageName.initialSetup.rawValue: { [weak self] in try await self?.initialSetup(params: $0, original: $1) }, + MessageName.open.rawValue: { [weak self] in try await self?.open(params: $0, original: $1) }, MessageName.reportInitException.rawValue: { [weak self] in try await self?.reportException(params: $0, original: $1) }, MessageName.reportPageException.rawValue: { [weak self] in try await self?.reportException(params: $0, original: $1) }, MessageName.widgetsSetConfig.rawValue: { [weak self] in try await self?.widgetsSetConfig(params: $0, original: $1) } @@ -175,6 +184,14 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { return nil } + private func open(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let openAction: NewTabPageDataModel.OpenAction = DecodableHelper.decode(from: params) else { + return nil + } + await linkOpener.openLink(openAction.target) + return nil + } + private func reportException(params: Any, original: WKScriptMessage) async throws -> Encodable? { guard let params = params as? [String: String] else { return nil } let message = params["message"] ?? "" diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift index 91a5973953..f6037002af 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift @@ -18,6 +18,16 @@ import Foundation +public extension NewTabPageDataModel { + struct OpenAction: Codable { + let target: Target + + public enum Target: String, Codable { + case settings + } + } +} + extension NewTabPageDataModel { enum WidgetId: String, Codable { From 2a2bb22a361a62d4f920db7be1a73dde0679c681 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 18 Dec 2024 23:23:36 +0100 Subject: [PATCH 44/62] Add NewTabPageCustomizerDisplayer --- .../HomePageSettingsModel.swift | 2 ++ .../NewTabPageCustomizationProvider.swift | 4 +++ .../Model/AppearancePreferences.swift | 5 +++ .../NewTabPageCustomBackgroundClient.swift | 15 +++++++++ .../NewTabPageCustomizerDisplayer.swift | 33 +++++++++++++++++++ .../NewTabPage/NewTabPageActionsManager.swift | 12 +++++++ 6 files changed, 71 insertions(+) create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift diff --git a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift index a693074e8a..568d0fea8d 100644 --- a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift @@ -18,6 +18,7 @@ import Combine import Foundation +import NewTabPage import os.log import PixelKit import SwiftUI @@ -88,6 +89,7 @@ extension HomePage.Models { let userColorProvider: () -> UserColorProviding let showAddImageFailedAlert: () -> Void let navigator: HomePageSettingsModelNavigator + let customizerOpener = NewTabPageCustomizerOpener() @Published var settingsButtonWidth: CGFloat = .infinity @Published private(set) var availableUserBackgroundImages: [UserBackgroundImage] = [] diff --git a/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift b/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift index e755bdeec0..d12faf0816 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift @@ -29,6 +29,10 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding self.appearancePreferences = appearancePreferences } + var customizerOpener: NewTabPageCustomizerOpener { + homePageSettingsModel.customizerOpener + } + var customizerData: NewTabPageDataModel.CustomizerData { .init( background: .init(homePageSettingsModel.customBackground), diff --git a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift index 8f87ad4e1c..ebfaef6897 100644 --- a/DuckDuckGo/Preferences/Model/AppearancePreferences.swift +++ b/DuckDuckGo/Preferences/Model/AppearancePreferences.swift @@ -117,6 +117,11 @@ final class DefaultHomePageNavigator: HomePageNavigator { if let window = WindowControllersManager.shared.lastKeyMainWindowController { let homePageViewController = window.mainViewController.browserTabViewController.homePageViewController homePageViewController?.settingsVisibilityModel.isSettingsVisible = true + + if NSApp.delegateTyped.featureFlagger.isFeatureOn(.htmlNewTabPage) { + let newTabPageViewModel = window.mainViewController.browserTabViewController.newTabPageWebViewModel + NSApp.delegateTyped.homePageSettingsModel.customizerOpener.openSettings(for: newTabPageViewModel.webView) + } } } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift index a635b3d2b0..9f86ec64e9 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomBackgroundClient.swift @@ -22,6 +22,7 @@ import UserScript import WebKit public protocol NewTabPageCustomBackgroundProviding: AnyObject { + var customizerOpener: NewTabPageCustomizerOpener { get } var customizerData: NewTabPageDataModel.CustomizerData { get } var background: NewTabPageDataModel.Background { get set } @@ -69,9 +70,18 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { } } .store(in: &cancellables) + + model.customizerOpener.openSettingsPublisher + .sink { [weak self] webView in + Task { @MainActor in + self?.openSettings(in: webView) + } + } + .store(in: &cancellables) } enum MessageName: String, CaseIterable { + case autoOpen = "customizer_autoOpen" case deleteImage = "customizer_deleteImage" case onBackgroundUpdate = "customizer_onBackgroundUpdate" case onImagesUpdate = "customizer_onImagesUpdate" @@ -137,4 +147,9 @@ public final class NewTabPageCustomBackgroundClient: NewTabPageScriptClient { private func notifyImagesUpdated(_ images: [NewTabPageDataModel.UserImage]) { pushMessage(named: MessageName.onImagesUpdate.rawValue, params: NewTabPageDataModel.UserImagesData(userImages: images)) } + + @MainActor + private func openSettings(in webView: WKWebView) { + pushMessage(named: MessageName.autoOpen.rawValue, params: nil, to: webView) + } } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift new file mode 100644 index 0000000000..3e27cf0563 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift @@ -0,0 +1,33 @@ +// +// NewTabPageCustomizerDisplayer.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 WebKit + +public final class NewTabPageCustomizerOpener { + public init() { + openSettingsPublisher = openSettingsSubject.eraseToAnyPublisher() + } + + public func openSettings(for webView: WKWebView) { + openSettingsSubject.send(webView) + } + + let openSettingsPublisher: AnyPublisher + private let openSettingsSubject = PassthroughSubject() +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift index 6903b36648..135a7bf6a0 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageActionsManager.swift @@ -20,6 +20,7 @@ import Foundation import Combine import Common import os.log +import WebKit /** * This protocol describes a feature or set of features that use HTML New Tab Page. @@ -56,6 +57,17 @@ public extension NewTabPageScriptClient { userScript.broker?.push(method: method, params: params, for: userScript, into: webView) } } + + /** + * Convenience method to push a message with specific parameters to the user script + * associated with the given `webView`. + */ + func pushMessage(named method: String, params: Encodable?, to webView: WKWebView) { + guard let userScript = userScriptsSource?.userScripts.first(where: { $0.webView === webView }) else { + return + } + userScript.broker?.push(method: method, params: params, for: userScript, into: webView) + } } /** From ead3f06cbd1ca89d9efb75bdc442f202883bc63e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Mon, 6 Jan 2025 19:03:39 +0100 Subject: [PATCH 45/62] Update tests --- ...ngNewTabPageCustomBackgroundProvider.swift | 2 ++ .../Mocks/CapturingNewTabPageLinkOpener.swift | 29 +++++++++++++++++++ .../NewTabPageConfigurationClientTests.swift | 3 +- 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift index d463aef60e..072fa781d1 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageCustomBackgroundProvider.swift @@ -20,6 +20,8 @@ import Combine import NewTabPage final class CapturingNewTabPageCustomBackgroundProvider: NewTabPageCustomBackgroundProviding { + var customizerOpener: NewTabPageCustomizerOpener = NewTabPageCustomizerOpener() + var customizerData: NewTabPageDataModel.CustomizerData = .init(background: .default, theme: .none, userColor: nil, userImages: []) @Published diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift new file mode 100644 index 0000000000..1430f2b5c6 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift @@ -0,0 +1,29 @@ +// +// CapturingNewTabPageLinkOpener.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 NewTabPage + +final class CapturingNewTabPageLinkOpener: NewTabPageLinkOpening { + func openLink(_ target: NewTabPageDataModel.OpenAction.Target) async { + openLinkCalls.append(target) + await _openLink(target) + } + + var openLinkCalls: [NewTabPageDataModel.OpenAction.Target] = [] + var _openLink: (NewTabPageDataModel.OpenAction.Target) async -> Void = { _ in } +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift index b4d9649748..bc12388d76 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift @@ -34,7 +34,8 @@ final class NewTabPageConfigurationClientTests: XCTestCase { client = NewTabPageConfigurationClient( sectionsVisibilityProvider: sectionsVisibilityProvider, customBackgroundProvider: CapturingNewTabPageCustomBackgroundProvider(), - contextMenuPresenter: contextMenuPresenter + contextMenuPresenter: contextMenuPresenter, + linkOpener: CapturingNewTabPageLinkOpener() ) userScript = NewTabPageUserScript() From 92289a187ee917ad09fde547050b121d5861c783 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 11:34:53 +0100 Subject: [PATCH 46/62] Add gradient02.01 gradient (that needs to go between gradient02 and gradient03) --- .../Contents.json | 12 ++++++ .../GradientTone02+01.jpg | Bin 0 -> 8438 bytes .../GradientBackground.swift | 39 +++++++++++++++++- .../HomePage/CustomBackgroundTests.swift | 1 + 4 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/GradientTone02+01.jpg diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/Contents.json new file mode 100644 index 0000000000..912fd7ad56 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "GradientTone02+01.jpg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/GradientTone02+01.jpg b/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/GradientTone02+01.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ee2e74fb3a5d99aca3d6588c6f8e1120ce3dbe7f GIT binary patch literal 8438 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 HXZHU;8wH-! literal 0 HcmV?d00001 diff --git a/DuckDuckGo/HomePage/Model/HomePageSettings/CustomBackgrounds/GradientBackground.swift b/DuckDuckGo/HomePage/Model/HomePageSettings/CustomBackgrounds/GradientBackground.swift index 31ce3e1275..403ea10734 100644 --- a/DuckDuckGo/HomePage/Model/HomePageSettings/CustomBackgrounds/GradientBackground.swift +++ b/DuckDuckGo/HomePage/Model/HomePageSettings/CustomBackgrounds/GradientBackground.swift @@ -26,6 +26,7 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch case gradient01 case gradient02 + case gradient0201 = "gradient02.01" case gradient03 case gradient04 case gradient05 @@ -46,6 +47,8 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch Gradient01() case .gradient02: Gradient02() + case .gradient0201: + Gradient0201() case .gradient03: Gradient03() case .gradient04: @@ -68,6 +71,8 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch Image(nsImage: .homePageBackgroundGradient01) case .gradient02: Image(nsImage: .homePageBackgroundGradient02) + case .gradient0201: + Image(nsImage: .homePageBackgroundGradient0201) case .gradient03: Image(nsImage: .homePageBackgroundGradient03) case .gradient04: @@ -83,7 +88,7 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch var colorScheme: ColorScheme { switch self { - case .gradient01, .gradient02, .gradient03: + case .gradient01, .gradient02, .gradient0201, .gradient03: .light case .gradient04, .gradient05, .gradient06, .gradient07: .dark @@ -191,6 +196,35 @@ private struct Gradient02: View { } } +@available(macOS 12.0, *) +private struct Gradient0201: View { + var body: some View { + ZStack { + EllipticalGradient( + colors: [Color(red: 1, green: 0.8, blue: 0.2).opacity(0.8), .clear], + center: UnitPoint(x: 1.04, y: 1.08), + endRadiusFraction: 1 + ) + + EllipticalGradient( + colors: [ + Color(red: 1, green: 0.84, blue: 0.36).opacity(0.7), + Color(red: 1, green: 0.84, blue: 0.8).opacity(0.2) + ], + center: UnitPoint(x: 0.56, y: 0.5), + endRadiusFraction: 1 + ) + + EllipticalGradient( + colors: [Color(red: 0.95, green: 0.63, blue: 0.54).opacity(0.6), .clear], + center: UnitPoint(x: -0.26, y: 0.5), + endRadiusFraction: 1 + ) + } + .background(Color(red: 1, green: 0.87, blue: 0.48)) + } +} + @available(macOS 12.0, *) private struct Gradient03: View { var body: some View { @@ -320,6 +354,7 @@ private struct Gradient06: View { .background(Color(red: 0.07, green: 0.01, blue: 0.21)) } } + @available(macOS 12.0, *) private struct Gradient07: View { var body: some View { @@ -358,6 +393,8 @@ private struct Gradient07: View { .frame(width: 640, height: 400) GradientBackground.gradient02.view .frame(width: 640, height: 400) + GradientBackground.gradient0201.view + .frame(width: 640, height: 400) GradientBackground.gradient03.view .frame(width: 640, height: 400) GradientBackground.gradient04.view diff --git a/UnitTests/HomePage/CustomBackgroundTests.swift b/UnitTests/HomePage/CustomBackgroundTests.swift index e5c063bc3d..b687503b28 100644 --- a/UnitTests/HomePage/CustomBackgroundTests.swift +++ b/UnitTests/HomePage/CustomBackgroundTests.swift @@ -62,6 +62,7 @@ final class CustomBackgroundTests: XCTestCase { func testDescriptionInitializer() { XCTAssertEqual(CustomBackground("gradient|gradient03"), .gradient(.gradient03)) + XCTAssertEqual(CustomBackground("gradient|gradient02.01"), .gradient(.gradient0201)) XCTAssertEqual(CustomBackground("solidColor|color02"), .solidColor(.color02)) XCTAssertEqual(CustomBackground("solidColor|#FEFC4B"), .solidColor(.init(color: NSColor(hex: "#FEFC4B")!))) From aef08a0abfd80f977075941e8fc19e0dd037acc5 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 15:51:21 +0100 Subject: [PATCH 47/62] Update BSK ref for code review --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +- .../xcshareddata/swiftpm/Package.resolved | 377 +++++++++--------- 2 files changed, 194 insertions(+), 189 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 4b51a95d0a..f2bf685e0c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3766,7 +3766,6 @@ 37BF3F13286D8A6500BD9014 /* PinnedTabsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTabsManager.swift; sourceTree = ""; }; 37BF3F1E286F0A7A00BD9014 /* PinnedTabsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsViewModel.swift; sourceTree = ""; }; 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedTabsView.swift; sourceTree = ""; }; - 37C33BCF2D01E92B00252656 /* content-scope-scripts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "content-scope-scripts"; path = "../content-scope-scripts"; sourceTree = SOURCE_ROOT; }; 37C9F78B2CF1C770004D73A1 /* PrivacyStatsTabExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyStatsTabExtension.swift; sourceTree = ""; }; 37CBCA992A8966E60050218F /* SyncSettingsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncSettingsAdapter.swift; sourceTree = ""; }; 37CC53EB27E8A4D10028713D /* PreferencesDataClearingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesDataClearingView.swift; sourceTree = ""; }; @@ -7914,7 +7913,6 @@ AA585D75248FD31100E9A3E2 = { isa = PBXGroup; children = ( - 37C33BCF2D01E92B00252656 /* content-scope-scripts */, 378B5886295CF2A4002C0CC0 /* Configuration */, 378E279C2970217400FCADA2 /* LocalPackages */, 7BB108552A43375D000AB95F /* LocalThirdParty */, @@ -15355,8 +15353,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 223.0.0; + branch = dominik/customizer; + 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 463c8af8b1..c7b8e09e4b 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,187 +1,194 @@ { - "object": { - "pins": [ - { - "package": "AppleToolbox", - "repositoryURL": "https://github.com/duckduckgo/apple-toolbox.git", - "state": { - "branch": null, - "revision": "0c13c5f056805f2d403618ccc3bfb833c303c68d", - "version": "3.1.2" - } - }, - { - "package": "BareBonesBrowserKit", - "repositoryURL": "https://github.com/duckduckgo/BareBonesBrowser.git", - "state": { - "branch": null, - "revision": "31e5bfedc3c2ca005640c4bf2b6959d69b0e18b9", - "version": "0.1.0" - } - }, - { - "package": "BloomFilter", - "repositoryURL": "https://github.com/duckduckgo/bloom_cpp.git", - "state": { - "branch": null, - "revision": "8076199456290b61b4544bf2f4caf296759906a0", - "version": "3.0.0" - } - }, - { - "package": "BrowserServicesKit", - "repositoryURL": "https://github.com/duckduckgo/BrowserServicesKit", - "state": { - "branch": null, - "revision": "e8f94cf597f4a447f86f39f461b736ac9ea280ce", - "version": "223.0.0" - } - }, - { - "package": "Autofill", - "repositoryURL": "https://github.com/duckduckgo/duckduckgo-autofill.git", - "state": { - "branch": null, - "revision": "88982a3802ac504e2f1a118a73bfdf2d8f4a7735", - "version": "16.0.0" - } - }, - { - "package": "GRDB", - "repositoryURL": "https://github.com/duckduckgo/GRDB.swift.git", - "state": { - "branch": null, - "revision": "5b2f6a81099d26ae0f9e38788f51490cd6a4b202", - "version": "2.4.2" - } - }, - { - "package": "Gzip", - "repositoryURL": "https://github.com/1024jp/GzipSwift.git", - "state": { - "branch": null, - "revision": "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", - "version": "6.0.1" - } - }, - { - "package": "Lottie", - "repositoryURL": "https://github.com/airbnb/lottie-spm.git", - "state": { - "branch": null, - "revision": "1d29eccc24cc8b75bff9f6804155112c0ffc9605", - "version": "4.4.3" - } - }, - { - "package": "OHHTTPStubs", - "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git", - "state": { - "branch": null, - "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9", - "version": "9.1.0" - } - }, - { - "package": "OpenSSL", - "repositoryURL": "https://github.com/duckduckgo/OpenSSL-XCFramework", - "state": { - "branch": null, - "revision": "71d303cbfa150e1fac99ffc7b4f67aad9c7a5002", - "version": "3.1.5004" - } - }, - { - "package": "PrivacyDashboardResources", - "repositoryURL": "https://github.com/duckduckgo/privacy-dashboard", - "state": { - "branch": null, - "revision": "2e2baf7d31c7d8e158a58bc1cb79498c1c727fd2", - "version": "7.5.0" - } - }, - { - "package": "Punycode", - "repositoryURL": "https://github.com/gumob/PunycodeSwift.git", - "state": { - "branch": null, - "revision": "30a462bdb4398ea835a3585472229e0d74b36ba5", - "version": "3.0.0" - } - }, - { - "package": "Sparkle", - "repositoryURL": "https://github.com/sparkle-project/Sparkle.git", - "state": { - "branch": null, - "revision": "b456fd404954a9e13f55aa0c88cd5a40b8399638", - "version": "2.6.3" - } - }, - { - "package": "swift-argument-parser", - "repositoryURL": "https://github.com/apple/swift-argument-parser.git", - "state": { - "branch": null, - "revision": "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", - "version": "1.4.0" - } - }, - { - "package": "swift-snapshot-testing", - "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing", - "state": { - "branch": null, - "revision": "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd", - "version": "1.15.4" - } - }, - { - "package": "swift-syntax", - "repositoryURL": "https://github.com/apple/swift-syntax", - "state": { - "branch": null, - "revision": "64889f0c732f210a935a0ad7cda38f77f876262d", - "version": "509.1.1" - } - }, - { - "package": "Swifter", - "repositoryURL": "https://github.com/httpswift/swifter.git", - "state": { - "branch": null, - "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", - "version": "1.5.0" - } - }, - { - "package": "DDGSyncCrypto", - "repositoryURL": "https://github.com/duckduckgo/sync_crypto", - "state": { - "branch": null, - "revision": "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", - "version": "0.3.0" - } - }, - { - "package": "TrackerRadarKit", - "repositoryURL": "https://github.com/duckduckgo/TrackerRadarKit", - "state": { - "branch": null, - "revision": "5de0a610a7927b638a5fd463a53032c9934a2c3b", - "version": "3.0.0" - } - }, - { - "package": "WireGuardKit", - "repositoryURL": "https://github.com/duckduckgo/wireguard-apple", - "state": { - "branch": null, - "revision": "13fd026384b1af11048451061cc1b21434990668", - "version": "1.1.3" - } - } - ] - }, - "version": 1 + "pins" : [ + { + "identity" : "apple-toolbox", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/apple-toolbox.git", + "state" : { + "revision" : "0c13c5f056805f2d403618ccc3bfb833c303c68d", + "version" : "3.1.2" + } + }, + { + "identity" : "barebonesbrowser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BareBonesBrowser.git", + "state" : { + "revision" : "31e5bfedc3c2ca005640c4bf2b6959d69b0e18b9", + "version" : "0.1.0" + } + }, + { + "identity" : "bloom_cpp", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/bloom_cpp.git", + "state" : { + "revision" : "8076199456290b61b4544bf2f4caf296759906a0", + "version" : "3.0.0" + } + }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/BrowserServicesKit", + "state" : { + "branch" : "dominik/customizer", + "revision" : "10667072437cf2c17938cda342859f0cbd5db499" + } + }, + { + "identity" : "content-scope-scripts", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/content-scope-scripts", + "state" : { + "branch" : "pr-releases/pr-1336", + "revision" : "93f6b5e3bb805b55f02f6c84c9f9fcd98b87bd11" + } + }, + { + "identity" : "duckduckgo-autofill", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/duckduckgo-autofill.git", + "state" : { + "revision" : "88982a3802ac504e2f1a118a73bfdf2d8f4a7735", + "version" : "16.0.0" + } + }, + { + "identity" : "grdb.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/GRDB.swift.git", + "state" : { + "revision" : "5b2f6a81099d26ae0f9e38788f51490cd6a4b202", + "version" : "2.4.2" + } + }, + { + "identity" : "gzipswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/1024jp/GzipSwift.git", + "state" : { + "revision" : "731037f6cc2be2ec01562f6597c1d0aa3fe6fd05", + "version" : "6.0.1" + } + }, + { + "identity" : "lottie-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/airbnb/lottie-spm.git", + "state" : { + "revision" : "1d29eccc24cc8b75bff9f6804155112c0ffc9605", + "version" : "4.4.3" + } + }, + { + "identity" : "ohhttpstubs", + "kind" : "remoteSourceControl", + "location" : "https://github.com/AliSoftware/OHHTTPStubs.git", + "state" : { + "revision" : "12f19662426d0434d6c330c6974d53e2eb10ecd9", + "version" : "9.1.0" + } + }, + { + "identity" : "openssl-xcframework", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/OpenSSL-XCFramework", + "state" : { + "revision" : "71d303cbfa150e1fac99ffc7b4f67aad9c7a5002", + "version" : "3.1.5004" + } + }, + { + "identity" : "privacy-dashboard", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/privacy-dashboard", + "state" : { + "revision" : "022c845b06ace6a4aa712a4fa3e79da32193d5c6", + "version" : "7.4.0" + } + }, + { + "identity" : "punycodeswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gumob/PunycodeSwift.git", + "state" : { + "revision" : "30a462bdb4398ea835a3585472229e0d74b36ba5", + "version" : "3.0.0" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle.git", + "state" : { + "revision" : "b456fd404954a9e13f55aa0c88cd5a40b8399638", + "version" : "2.6.3" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", + "version" : "1.4.0" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd", + "version" : "1.15.4" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", + "version" : "509.1.1" + } + }, + { + "identity" : "swifter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/httpswift/swifter.git", + "state" : { + "revision" : "9483a5d459b45c3ffd059f7b55f9638e268632fd", + "version" : "1.5.0" + } + }, + { + "identity" : "sync_crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/sync_crypto", + "state" : { + "revision" : "0c8bf3c0e75591bc366407b9d7a73a9fcfc7736f", + "version" : "0.3.0" + } + }, + { + "identity" : "trackerradarkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "state" : { + "revision" : "5de0a610a7927b638a5fd463a53032c9934a2c3b", + "version" : "3.0.0" + } + }, + { + "identity" : "wireguard-apple", + "kind" : "remoteSourceControl", + "location" : "https://github.com/duckduckgo/wireguard-apple", + "state" : { + "revision" : "13fd026384b1af11048451061cc1b21434990668", + "version" : "1.1.3" + } + } + ], + "version" : 2 } From edd899f5cd452fbce4c6d585e18a2a47bd32140d Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 16:05:12 +0100 Subject: [PATCH 48/62] Update BSK ref --- .../xcshareddata/swiftpm/Package.resolved | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c7b8e09e4b..5f43dbbd7d 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/customizer", - "revision" : "10667072437cf2c17938cda342859f0cbd5db499" + "revision" : "dd145b68bcd90a76cc50f7e5b96819902febf927" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/privacy-dashboard", "state" : { - "revision" : "022c845b06ace6a4aa712a4fa3e79da32193d5c6", - "version" : "7.4.0" + "revision" : "2e2baf7d31c7d8e158a58bc1cb79498c1c727fd2", + "version" : "7.5.0" } }, { From 43acfe2369c193b5afa5f88dd2f50cd1f9b5251c Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 16:13:17 +0100 Subject: [PATCH 49/62] Update BSK ref --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f2bf685e0c..597c81d42b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -15353,8 +15353,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - branch = dominik/customizer; - kind = branch; + kind = exactVersion; + version = 223.0.0; }; }; 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 5f43dbbd7d..abbc299c08 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "branch" : "dominik/customizer", - "revision" : "dd145b68bcd90a76cc50f7e5b96819902febf927" + "revision" : "e8f94cf597f4a447f86f39f461b736ac9ea280ce", + "version" : "223.0.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "branch" : "pr-releases/pr-1336", - "revision" : "93f6b5e3bb805b55f02f6c84c9f9fcd98b87bd11" + "revision" : "bc808eb735d9eb72d5c54cf2452b104b6a370e25", + "version" : "6.43.0" } }, { From a408caa031a4233618f039bb74ca83b72ad5cb31 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 17:38:50 +0100 Subject: [PATCH 50/62] Update C-S-S to 7.1.0 --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- .../xcshareddata/swiftpm/Package.resolved | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 597c81d42b..f2bf685e0c 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -15353,8 +15353,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 223.0.0; + branch = dominik/customizer; + 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 abbc299c08..eff71891b4 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/BrowserServicesKit", "state" : { - "revision" : "e8f94cf597f4a447f86f39f461b736ac9ea280ce", - "version" : "223.0.0" + "branch" : "dominik/customizer", + "revision" : "68201f76152e1d65bc03ec2aae142feffdc07e4b" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/duckduckgo/content-scope-scripts", "state" : { - "revision" : "bc808eb735d9eb72d5c54cf2452b104b6a370e25", - "version" : "6.43.0" + "revision" : "a539758027d9fd37d9d26213399ac156ca9fb81c", + "version" : "7.1.0" } }, { From abd13147bd2406622d9c629a7b9b9b768e0812dd Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 17:58:13 +0100 Subject: [PATCH 51/62] Update gradient 02.01 asset --- .../GradientTone02+01.jpg | Bin 8438 -> 74163 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/GradientTone02+01.jpg b/DuckDuckGo/Assets.xcassets/Images/HomePage/Gradients/HomePageBackgroundGradient02+01.imageset/GradientTone02+01.jpg index ee2e74fb3a5d99aca3d6588c6f8e1120ce3dbe7f..2c2da7d36a21451965461d17ecd0de11a43045f9 100644 GIT binary patch literal 74163 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 HXZHU;8wH-! From 9a456076d1d6fbed79e4841726c036919570cb72 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Tue, 7 Jan 2025 18:59:15 +0100 Subject: [PATCH 52/62] Add Freemium PIR banner to HTML New Tab Page (#3706) Task/Issue URL: https://app.asana.com/0/72649045549333/1208973375734052/f Description: This change adds support for displaying Freemium PIR banner in the HTML New Tab Page. The logic of FreemiumDBPPromotionViewCoordinator is updated so that the viewModel is a published property, updated whenever isHomePagePromotionVisible changes (which also means that it's nullified whenever the user dismisses or actions a banner). --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + .../PromotionView+FreemiumDBP.swift | 6 +- .../FreemiumDBPPromotionViewCoordinator.swift | 30 +++-- .../HomePage/Model/PromotionViewModel.swift | 4 +- DuckDuckGo/HomePage/View/PromotionView.swift | 6 +- .../NewTabPageActionsManagerExtension.swift | 2 + .../NewTabPageFreemiumDBPBannerProvider.swift | 66 +++++++++++ .../NewTabPage/NewTabPageWebViewModel.swift | 7 +- .../NewTabPageConfigurationClient.swift | 1 + .../NewTabPageDataModel+Configuration.swift | 6 +- .../NewTabPageDataModel+FreemiumDBP.swift | 43 +++++++ .../NewTabPageFreemiumDBPClient.swift | 89 ++++++++++++++ .../NewTabPage/NewTabPageDataModel.swift | 1 - ...gNewTabPageFreemiumDBPBannerProvider.swift | 47 ++++++++ .../Mocks/CapturingNewTabPageLinkOpener.swift | 1 + .../NewTabPageActionsManagerTests.swift | 2 +- .../NewTabPageConfigurationClientTests.swift | 9 +- ...ewTabPageCustomBackgroundClientTests.swift | 6 +- .../NewTabPageFavoritesClientTests.swift | 10 +- .../NewTabPageFavoritesModelTests.swift | 6 +- .../NewTabPageFreemiumDBPClientTests.swift | 78 +++++++++++++ .../NewTabPageNextStepsCardsClientTests.swift | 6 +- .../NewTabPagePrivacyStatsClientTests.swift | 14 +-- .../NewTabPagePrivacyStatsModelTests.swift | 8 +- .../NewTabPageRMFClientTests.swift | 6 +- ...miumDBPPromotionViewCoordinatorTests.swift | 110 ++++++++++++------ UnitTests/Menus/MoreOptionsMenuTests.swift | 6 +- 27 files changed, 486 insertions(+), 90 deletions(-) create mode 100644 DuckDuckGo/NewTabPage/NewTabPageFreemiumDBPBannerProvider.swift create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageDataModel+FreemiumDBP.swift create mode 100644 LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageFreemiumDBPClient.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFreemiumDBPBannerProvider.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 597c81d42b..864d52ce2b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1159,6 +1159,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 */; }; + 3758A38C2D108233001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */; }; + 3758A38D2D108233001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.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 */; }; @@ -3698,6 +3700,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 = ""; }; + 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageFreemiumDBPBannerProvider.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 = ""; }; @@ -5875,6 +5878,7 @@ isa = PBXGroup; children = ( 37F9AEB02D1316FE007FD19B /* DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.swift */, + 3758A38B2D108229001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift */, 371E1D662D0B2E9F00F9205B /* NewTabPageCustomizationProvider.swift */, 37BE08792D09C0EA00C77B8E /* NewTabPageNextStepsCardsProvider.swift */, 37E307B12D075B5E00599500 /* NewTabPagePrivacyStatsEventHandler.swift */, @@ -12264,6 +12268,7 @@ 372A0FED2B2379310033BF7F /* SyncMetricsEventsHandler.swift in Sources */, 3706FCA4293F65D500E42796 /* RecentlyClosedMenu.swift in Sources */, 8400DC4C2C6E26AE006509D2 /* ItemCachingCollectionView.swift in Sources */, + 3758A38C2D108233001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift in Sources */, 4B9DB02D2A983B24000927DB /* WaitlistKeychainStorage.swift in Sources */, C181945D2C7CDCC700381092 /* PromotionView.swift in Sources */, ); @@ -13855,6 +13860,7 @@ B6ABD0CE2BC042CE0000EB69 /* NSURL+sandboxExtensionRetainCount.m in Sources */, 4B4D60DF2A0C875F00BCD287 /* NetworkProtectionOptionKeyExtension.swift in Sources */, 85AC3AEF25D5CE9800C7D2AA /* UserScripts.swift in Sources */, + 3758A38D2D108233001CEAA1 /* NewTabPageFreemiumDBPBannerProvider.swift in Sources */, B643BF1427ABF772000BACEC /* NSWorkspaceExtension.swift in Sources */, B6C00ECB292F839D009C73A6 /* AutofillTabExtension.swift in Sources */, B6E319382953446000DD3BCF /* Assertions.swift in Sources */, diff --git a/DuckDuckGo/Freemium/DBP/Extensions/PromotionView+FreemiumDBP.swift b/DuckDuckGo/Freemium/DBP/Extensions/PromotionView+FreemiumDBP.swift index c9119ed6b1..807271c3dc 100644 --- a/DuckDuckGo/Freemium/DBP/Extensions/PromotionView+FreemiumDBP.swift +++ b/DuckDuckGo/Freemium/DBP/Extensions/PromotionView+FreemiumDBP.swift @@ -19,7 +19,7 @@ import Foundation extension PromotionViewModel { - static func freemiumDBPPromotion(proceedAction: @escaping () -> Void, + static func freemiumDBPPromotion(proceedAction: @escaping () async -> Void, closeAction: @escaping () -> Void) -> PromotionViewModel { let title = UserText.homePagePromotionFreemiumDBPTitle @@ -41,7 +41,7 @@ extension PromotionViewModel { static func freemiumDBPPromotionScanEngagementResults(resultCount: Int, brokerCount: Int, - proceedAction: @escaping () -> Void, + proceedAction: @escaping () async -> Void, closeAction: @escaping () -> Void) -> PromotionViewModel { var description = "" @@ -65,7 +65,7 @@ extension PromotionViewModel { closeAction: closeAction) } - static func freemiumDBPPromotionScanEngagementNoResults(proceedAction: @escaping () -> Void, + static func freemiumDBPPromotionScanEngagementNoResults(proceedAction: @escaping () async -> Void, closeAction: @escaping () -> Void) -> PromotionViewModel { let description = UserText.homePagePromotionFreemiumDBPPostScanEngagementNoResultsDescription diff --git a/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift b/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift index 56631b6b0c..d49414b0b1 100644 --- a/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift +++ b/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift @@ -25,17 +25,14 @@ import Common /// Default implementation of `FreemiumDBPPromotionViewCoordinating`, responsible for managing /// the visibility of the promotion and responding to user interactions with the promotion view. -@MainActor final class FreemiumDBPPromotionViewCoordinator: ObservableObject { /// Published property that determines whether the promotion is visible on the home page. @Published var isHomePagePromotionVisible: Bool = false /// The view model representing the promotion, which updates based on the user's state. Returns `nil` if the feature is not enabled - var viewModel: PromotionViewModel? { - guard freemiumDBPFeature.isAvailable else { return nil } - return createViewModel() - } + @Published + private(set) var viewModel: PromotionViewModel? /// Stores whether the user has dismissed the home page promotion. private var didDismissHomePagePromotion: Bool { @@ -89,13 +86,14 @@ final class FreemiumDBPPromotionViewCoordinator: ObservableObject { setInitialPromotionVisibilityState() subscribeToFeatureAvailabilityUpdates() observeFreemiumDBPNotifications() + setUpViewModelRefreshing() } } private extension FreemiumDBPPromotionViewCoordinator { /// Action to be executed when the user proceeds with the promotion (e.g opens DBP) - var proceedAction: () -> Void { + var proceedAction: () async -> Void { { [weak self] in guard let self else { return } @@ -107,7 +105,7 @@ private extension FreemiumDBPPromotionViewCoordinator { self.freemiumDBPExperimentPixelHandler.fire(FreemiumDBPExperimentPixel.newTabScanClick) }) - showFreemiumDBP() + await showFreemiumDBP() dismissHomePagePromotion() } } @@ -130,6 +128,7 @@ private extension FreemiumDBPPromotionViewCoordinator { } /// Shows the Freemium DBP user interface via the presenter. + @MainActor func showFreemiumDBP() { freemiumDBPPresenter.showFreemiumDBPAndSetActivated(windowControllerManager: WindowControllersManager.shared) } @@ -148,7 +147,10 @@ private extension FreemiumDBPPromotionViewCoordinator { /// Creates the view model for the promotion, updating based on the user's scan results. /// /// - Returns: The `PromotionViewModel` that represents the current state of the promotion. - func createViewModel() -> PromotionViewModel { + func createViewModel() -> PromotionViewModel? { + guard freemiumDBPFeature.isAvailable, isHomePagePromotionVisible else { + return nil + } if let results = freemiumDBPUserStateManager.firstScanResults { if results.matchesCount > 0 { @@ -172,12 +174,24 @@ private extension FreemiumDBPPromotionViewCoordinator { } } + /// This method defines the entry point to updating `viewModel` which is every change to `isHomePagePromotionVisible`. + func setUpViewModelRefreshing() { + $isHomePagePromotionVisible.dropFirst().asVoid() + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.viewModel = self?.createViewModel() + } + .store(in: &cancellables) + } + /// Subscribes to feature availability updates from the `freemiumDBPFeature`'s availability publisher. /// /// This method listens to the `isAvailablePublisher` of the `freemiumDBPFeature`, which publishes /// changes to the feature's availability. It performs the following actions when an update is received: func subscribeToFeatureAvailabilityUpdates() { freemiumDBPFeature.isAvailablePublisher + .prepend(freemiumDBPFeature.isAvailable) + .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] isAvailable in guard let self else { return } diff --git a/DuckDuckGo/HomePage/Model/PromotionViewModel.swift b/DuckDuckGo/HomePage/Model/PromotionViewModel.swift index b0f57261cb..0a43082af0 100644 --- a/DuckDuckGo/HomePage/Model/PromotionViewModel.swift +++ b/DuckDuckGo/HomePage/Model/PromotionViewModel.swift @@ -27,10 +27,10 @@ extension HomePage.Models { let title: String? let description: String let proceedButtonText: String - let proceedAction: () -> Void + let proceedAction: () async -> Void let closeAction: () -> Void - init(image: ImageResource, title: String? = nil, description: String, proceedButtonText: String, proceedAction: @escaping () -> Void, closeAction: @escaping () -> Void) { + init(image: ImageResource, title: String? = nil, description: String, proceedButtonText: String, proceedAction: @escaping () async -> Void, closeAction: @escaping () -> Void) { self.image = image self.title = title self.description = description diff --git a/DuckDuckGo/HomePage/View/PromotionView.swift b/DuckDuckGo/HomePage/View/PromotionView.swift index 13b8b646b6..1b13010342 100644 --- a/DuckDuckGo/HomePage/View/PromotionView.swift +++ b/DuckDuckGo/HomePage/View/PromotionView.swift @@ -108,7 +108,11 @@ extension HomePage.Views { private var button: some View { Group { - Button(action: viewModel.proceedAction) { + Button { + Task { @MainActor in + await viewModel.proceedAction() + } + } label: { Text(verbatim: viewModel.proceedButtonText) } .controlSize(.large) diff --git a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift index dcade5a53b..71f0290ce4 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift @@ -42,6 +42,7 @@ extension NewTabPageActionsManager { ) let customizationProvider = NewTabPageCustomizationProvider(homePageSettingsModel: NSApp.delegateTyped.homePageSettingsModel) + let freemiumDBPBannerProvider = NewTabPageFreemiumDBPBannerProvider(model: NSApp.delegateTyped.freemiumDBPPromotionViewCoordinator) self.init(scriptClients: [ NewTabPageConfigurationClient( @@ -51,6 +52,7 @@ extension NewTabPageActionsManager { ), NewTabPageCustomBackgroundClient(model: customizationProvider), NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel), + NewTabPageFreemiumDBPClient(provider: freemiumDBPBannerProvider), NewTabPageNextStepsCardsClient(model: NewTabPageNextStepsCardsProvider(continueSetUpModel: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener()))), NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: Int(Favicon.SizeCategory.medium.rawValue)), NewTabPagePrivacyStatsClient(model: privacyStatsModel) diff --git a/DuckDuckGo/NewTabPage/NewTabPageFreemiumDBPBannerProvider.swift b/DuckDuckGo/NewTabPage/NewTabPageFreemiumDBPBannerProvider.swift new file mode 100644 index 0000000000..1a93f53671 --- /dev/null +++ b/DuckDuckGo/NewTabPage/NewTabPageFreemiumDBPBannerProvider.swift @@ -0,0 +1,66 @@ +// +// NewTabPageFreemiumDBPBannerProvider.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 NewTabPage + +final class NewTabPageFreemiumDBPBannerProvider: NewTabPageFreemiumDBPBannerProviding { + + var bannerMessage: NewTabPageDataModel.FreemiumPIRBannerMessage? { + guard let viewModel = model.viewModel else { + return nil + } + return .init(viewModel) + } + + var bannerMessagePublisher: AnyPublisher { + model.$viewModel.dropFirst() + .map { viewModel in + guard let viewModel else { + return nil + } + return NewTabPageDataModel.FreemiumPIRBannerMessage(viewModel) + } + .eraseToAnyPublisher() + } + + func dismiss() async { + model.viewModel?.closeAction() + } + + func action() async { + await model.viewModel?.proceedAction() + } + + let model: FreemiumDBPPromotionViewCoordinator + + init(model: FreemiumDBPPromotionViewCoordinator) { + self.model = model + } +} + +extension NewTabPageDataModel.FreemiumPIRBannerMessage { + init(_ promotionViewModel: PromotionViewModel) { + + self.init( + titleText: promotionViewModel.title, + descriptionText: promotionViewModel.description, + actionText: promotionViewModel.proceedButtonText + ) + } +} diff --git a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift index e8eb6fdef0..5fb37935de 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift @@ -37,7 +37,11 @@ final class NewTabPageWebViewModel: NSObject { let webView: WebView private var windowCancellable: AnyCancellable? - init(featureFlagger: FeatureFlagger, actionsManager: NewTabPageActionsManaging, activeRemoteMessageModel: ActiveRemoteMessageModel) { + init( + featureFlagger: FeatureFlagger, + actionsManager: NewTabPageActionsManaging, + activeRemoteMessageModel: ActiveRemoteMessageModel + ) { newTabPageUserScript = NewTabPageUserScript() actionsManager.registerUserScript(newTabPageUserScript) @@ -55,6 +59,7 @@ final class NewTabPageWebViewModel: NSObject { .map { $0 != nil } .sink { [weak activeRemoteMessageModel] isOnScreen in activeRemoteMessageModel?.isViewOnScreen = isOnScreen + if isOnScreen { NotificationCenter.default.post(name: .newTabPageWebViewDidAppear, object: nil) } diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift index 166c590071..78cb6f9470 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageConfigurationClient.swift @@ -149,6 +149,7 @@ public final class NewTabPageConfigurationClient: NewTabPageScriptClient { let config = NewTabPageDataModel.NewTabPageConfiguration( widgets: [ .init(id: .rmf), + .init(id: .freemiumPIRBanner), .init(id: .nextSteps), .init(id: .favorites), .init(id: .privacyStats) diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift index f6037002af..ca96bc506f 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/Configuration/NewTabPageDataModel+Configuration.swift @@ -31,7 +31,7 @@ public extension NewTabPageDataModel { extension NewTabPageDataModel { enum WidgetId: String, Codable { - case rmf, nextSteps, favorites, privacyStats + case rmf, freemiumPIRBanner, nextSteps, favorites, privacyStats } struct ContextMenuParams: Codable { @@ -49,8 +49,8 @@ extension NewTabPageDataModel { var env: String var locale: String var platform: Platform - var settings: Settings - var customizer: NewTabPageDataModel.CustomizerData + var settings: Settings? + var customizer: NewTabPageDataModel.CustomizerData? struct Widget: Encodable, Equatable { public var id: WidgetId diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageDataModel+FreemiumDBP.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageDataModel+FreemiumDBP.swift new file mode 100644 index 0000000000..a5524e509c --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageDataModel+FreemiumDBP.swift @@ -0,0 +1,43 @@ +// +// NewTabPageDataModel+FreemiumDBP.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 + +public extension NewTabPageDataModel { + + struct FreemiumPIRBannerMessage: Encodable, Equatable { + let messageType = "big_single_action" + + let id = "banner_message" + let titleText: String? + let descriptionText: String + let actionText: String + + public init(titleText: String?, descriptionText: String, actionText: String) { + self.titleText = titleText + self.descriptionText = descriptionText + self.actionText = actionText + } + } +} + +extension NewTabPageDataModel { + struct FreemiumPIRBannerMessageData: Encodable, Equatable { + let content: FreemiumPIRBannerMessage? + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageFreemiumDBPClient.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageFreemiumDBPClient.swift new file mode 100644 index 0000000000..c472a31096 --- /dev/null +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/FreemiumDBP/NewTabPageFreemiumDBPClient.swift @@ -0,0 +1,89 @@ +// +// NewTabPageFreemiumDBPClient.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 Common +import UserScript +import WebKit + +public protocol NewTabPageFreemiumDBPBannerProviding { + + var bannerMessage: NewTabPageDataModel.FreemiumPIRBannerMessage? { get } + + var bannerMessagePublisher: AnyPublisher { get } + + func dismiss() async + + func action() async +} + +public final class NewTabPageFreemiumDBPClient: NewTabPageScriptClient { + + let freemiumDBPBannerProvider: NewTabPageFreemiumDBPBannerProviding + public weak var userScriptsSource: NewTabPageUserScriptsSource? + + private var cancellables = Set() + + public init(provider: NewTabPageFreemiumDBPBannerProviding) { + self.freemiumDBPBannerProvider = provider + + freemiumDBPBannerProvider.bannerMessagePublisher + .sink { [weak self] message in + self?.notifyMessageDidChange(message) + } + .store(in: &cancellables) + } + + enum MessageName: String, CaseIterable { + case getData = "freemiumPIRBanner_getData" + case onDataUpdate = "freemiumPIRBanner_onDataUpdate" + case dismiss = "freemiumPIRBanner_dismiss" + case action = "freemiumPIRBanner_action" + } + + public func registerMessageHandlers(for userScript: any SubfeatureWithExternalMessageHandling) { + userScript.registerMessageHandlers([ + MessageName.action.rawValue: { [weak self] in try await self?.action(params: $0, original: $1) }, + MessageName.dismiss.rawValue: { [weak self] in try await self?.dismiss(params: $0, original: $1) }, + MessageName.getData.rawValue: { [weak self] in try await self?.getData(params: $0, original: $1) }, + ]) + } + + @MainActor + private func getData(params: Any, original: WKScriptMessage) async throws -> Encodable? { + guard let message = freemiumDBPBannerProvider.bannerMessage else { + return NewTabPageDataModel.FreemiumPIRBannerMessageData(content: nil) + } + + return NewTabPageDataModel.FreemiumPIRBannerMessageData(content: message) + } + + private func dismiss(params: Any, original: WKScriptMessage) async throws -> Encodable? { + await freemiumDBPBannerProvider.dismiss() + return nil + } + + private func action(params: Any, original: WKScriptMessage) async throws -> Encodable? { + await freemiumDBPBannerProvider.action() + return nil + } + + private func notifyMessageDidChange(_ message: NewTabPageDataModel.FreemiumPIRBannerMessage?) { + pushMessage(named: MessageName.onDataUpdate.rawValue, params: NewTabPageDataModel.FreemiumPIRBannerMessageData(content: message)) + } +} diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift index 0fda4bb512..0cc6c1afdd 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/NewTabPageDataModel.swift @@ -17,4 +17,3 @@ // public enum NewTabPageDataModel {} - diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFreemiumDBPBannerProvider.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFreemiumDBPBannerProvider.swift new file mode 100644 index 0000000000..7e07ec9546 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageFreemiumDBPBannerProvider.swift @@ -0,0 +1,47 @@ +// +// CapturingNewTabPageFreemiumDBPBannerProvider.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 NewTabPage + +final class CapturingNewTabPageFreemiumDBPBannerProvider: NewTabPageFreemiumDBPBannerProviding { + @Published var bannerMessage: NewTabPageDataModel.FreemiumPIRBannerMessage? + + var bannerMessagePublisher: AnyPublisher { + $bannerMessage.dropFirst().eraseToAnyPublisher() + } + + func dismiss() async { + dismissCallCount += 1 + await _dismiss() + } + + func action() async { + actionCallCount += 1 + await _action() + } + + var dismissCallCount: Int = 0 + var actionCallCount: Int = 0 + + // swiftlint:disable identifier_name + var _dismiss: () async -> Void = {} + var _action: () async -> Void = {} + // swiftlint:enable identifier_name +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift index 1430f2b5c6..c4ce4fa852 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/CapturingNewTabPageLinkOpener.swift @@ -25,5 +25,6 @@ final class CapturingNewTabPageLinkOpener: NewTabPageLinkOpening { } var openLinkCalls: [NewTabPageDataModel.OpenAction.Target] = [] + // swiftlint:disable:next identifier_name var _openLink: (NewTabPageDataModel.OpenAction.Target) async -> Void = { _ in } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift index dddc979744..1c69484b92 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageActionsManagerTests.swift @@ -30,7 +30,7 @@ final class MockNewTabPageScriptClient: NewTabPageScriptClient { } final class NewTabPageActionsManagerTests: XCTestCase { - var actionsManager: NewTabPageActionsManager! + private var actionsManager: NewTabPageActionsManager! func testThatUserScriptsReturnsAllRegisteredUserScripts() { let actionsManager = NewTabPageActionsManager(scriptClients: []) diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift index bc12388d76..5460508f63 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift @@ -22,10 +22,10 @@ import XCTest @testable import NewTabPage final class NewTabPageConfigurationClientTests: XCTestCase { - var client: NewTabPageConfigurationClient! - var sectionsVisibilityProvider: MockNewTabPageSectionsVisibilityProvider! - var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! - var userScript: NewTabPageUserScript! + private var client: NewTabPageConfigurationClient! + private var sectionsVisibilityProvider: MockNewTabPageSectionsVisibilityProvider! + private var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! + private var userScript: NewTabPageUserScript! override func setUpWithError() throws { try super.setUpWithError() @@ -77,6 +77,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { let configuration: NewTabPageDataModel.NewTabPageConfiguration = try await sendMessage(named: .initialSetup) XCTAssertEqual(configuration.widgets, [ .init(id: .rmf), + .init(id: .freemiumPIRBanner), .init(id: .nextSteps), .init(id: .favorites), .init(id: .privacyStats) diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift index 38a3e3a675..5ea809676b 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift @@ -22,9 +22,9 @@ import XCTest @testable import NewTabPage final class NewTabPageCustomBackgroundClientTests: XCTestCase { - var client: NewTabPageCustomBackgroundClient! - var model: CapturingNewTabPageCustomBackgroundProvider! - var userScript: NewTabPageUserScript! + private var client: NewTabPageCustomBackgroundClient! + private var model: CapturingNewTabPageCustomBackgroundProvider! + private var userScript: NewTabPageUserScript! override func setUpWithError() throws { try super.setUpWithError() diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index 73024515dc..b49e9e63a3 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -25,11 +25,11 @@ import XCTest final class NewTabPageFavoritesClientTests: XCTestCase { typealias NewTabPageFavoritesClientUnderTest = NewTabPageFavoritesClient - var client: NewTabPageFavoritesClientUnderTest! - var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! - var actionsHandler: CapturingNewTabPageFavoritesActionsHandler! - var favoritesModel: NewTabPageFavoritesModel! - var userScript: NewTabPageUserScript! + private var client: NewTabPageFavoritesClientUnderTest! + private var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! + private var actionsHandler: CapturingNewTabPageFavoritesActionsHandler! + private var favoritesModel: NewTabPageFavoritesModel! + private var userScript: NewTabPageUserScript! @MainActor override func setUpWithError() throws { diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift index 7785c1928d..7b30d97f9d 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesModelTests.swift @@ -23,9 +23,9 @@ import XCTest final class NewTabPageFavoritesModelTests: XCTestCase { - var model: NewTabPageFavoritesModel! - var settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor! - var favoritesSubject: PassthroughSubject<[MockNewTabPageFavorite], Never>! + private var model: NewTabPageFavoritesModel! + private var settingsPersistor: UserDefaultsNewTabPageFavoritesSettingsPersistor! + private var favoritesSubject: PassthroughSubject<[MockNewTabPageFavorite], Never>! override func setUp() async throws { try await super.setUp() diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift new file mode 100644 index 0000000000..81a1443ed9 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift @@ -0,0 +1,78 @@ +// +// NewTabPageFreemiumDBPClientTests.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 RemoteMessaging +import XCTest +@testable import NewTabPage + +final class NewTabPageFreemiumDBPClientTests: XCTestCase { + private var client: NewTabPageFreemiumDBPClient! + private var provider: CapturingNewTabPageFreemiumDBPBannerProvider! + private var userScript: NewTabPageUserScript! + + override func setUpWithError() throws { + try super.setUpWithError() + provider = CapturingNewTabPageFreemiumDBPBannerProvider() + client = NewTabPageFreemiumDBPClient(provider: provider) + userScript = NewTabPageUserScript() + client.registerMessageHandlers(for: userScript) + } + + // MARK: - getData + + func testWhenMessageIsNilThenGetDataReturnsNilMessage() async throws { + let messageData: NewTabPageDataModel.FreemiumPIRBannerMessageData = try await sendMessage(named: .getData) + XCTAssertNil(messageData.content) + } + + func testThatGetDataReturnsMessageIfPresent() async throws { + provider.bannerMessage = .init(titleText: "sample_title", descriptionText: "sample_description", actionText: "sample_action") + let messageData: NewTabPageDataModel.FreemiumPIRBannerMessageData = try await sendMessage(named: .getData) + let message = try XCTUnwrap(messageData.content) + XCTAssertEqual(message, .init(titleText: "sample_title", descriptionText: "sample_description", actionText: "sample_action")) + } + + // MARK: - dismiss + + func testThatDismissIsForwardedToProvider() async throws { + try await sendMessageExpectingNilResponse(named: .dismiss) + XCTAssertEqual(provider.dismissCallCount, 1) + } + + // MARK: - action + + func testThatActionIsForwardedToProvider() async throws { + try await sendMessageExpectingNilResponse(named: .action) + XCTAssertEqual(provider.actionCallCount, 1) + } + + // MARK: - Helper functions + + func sendMessage(named methodName: NewTabPageFreemiumDBPClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + return try XCTUnwrap(response as? Response, file: file, line: line) + } + + func sendMessageExpectingNilResponse(named methodName: NewTabPageFreemiumDBPClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) + XCTAssertNil(response, file: file, line: line) + } +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift index df4cf389d0..c3605441cb 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift @@ -22,9 +22,9 @@ import XCTest @testable import NewTabPage final class NewTabPageNextStepsCardsClientTests: XCTestCase { - var client: NewTabPageNextStepsCardsClient! - var model: CapturingNewTabPageNextStepsCardsProvider! - var userScript: NewTabPageUserScript! + private var client: NewTabPageNextStepsCardsClient! + private var model: CapturingNewTabPageNextStepsCardsProvider! + private var userScript: NewTabPageUserScript! @MainActor override func setUpWithError() throws { diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift index ac4db71db0..d5edc36305 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift @@ -24,15 +24,15 @@ import XCTest @testable import NewTabPage final class NewTabPagePrivacyStatsClientTests: XCTestCase { - var client: NewTabPagePrivacyStatsClient! - var model: NewTabPagePrivacyStatsModel! + private var client: NewTabPagePrivacyStatsClient! + private var model: NewTabPagePrivacyStatsModel! - var privacyStats: CapturingPrivacyStats! - var trackerDataProvider: MockPrivacyStatsTrackerDataProvider! - var eventMapping: CapturingNewTabPagePrivacyStatsEventHandler! - var settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor! + private var privacyStats: CapturingPrivacyStats! + private var trackerDataProvider: MockPrivacyStatsTrackerDataProvider! + private var eventMapping: CapturingNewTabPagePrivacyStatsEventHandler! + private var settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor! - var userScript: NewTabPageUserScript! + private var userScript: NewTabPageUserScript! override func setUp() async throws { try await super.setUp() diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift index 6db5bbe685..b7173cddce 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsModelTests.swift @@ -67,10 +67,10 @@ final class MockPrivacyStatsTrackerDataProvider: PrivacyStatsTrackerDataProvidin final class NewTabPagePrivacyStatsModelTests: XCTestCase { - var model: NewTabPagePrivacyStatsModel! - var privacyStats: CapturingPrivacyStats! - var trackerDataProvider: MockPrivacyStatsTrackerDataProvider! - var settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor! + private var model: NewTabPagePrivacyStatsModel! + private var privacyStats: CapturingPrivacyStats! + private var trackerDataProvider: MockPrivacyStatsTrackerDataProvider! + private var settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor! override func setUp() async throws { try await super.setUp() diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift index 23a746c03a..1d6ccfee56 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift @@ -22,9 +22,9 @@ import XCTest @testable import NewTabPage final class NewTabPageRMFClientTests: XCTestCase { - var client: NewTabPageRMFClient! - var remoteMessageProvider: CapturingNewTabPageActiveRemoteMessageProvider! - var userScript: NewTabPageUserScript! + private var client: NewTabPageRMFClient! + private var remoteMessageProvider: CapturingNewTabPageActiveRemoteMessageProvider! + private var userScript: NewTabPageUserScript! override func setUpWithError() throws { try super.setUpWithError() diff --git a/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift b/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift index fca8c31aec..b565768f58 100644 --- a/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift +++ b/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift @@ -92,13 +92,16 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testProceedAction_dismissesPromotion_callsShowFreemium_andFiresPixel() { + func testProceedAction_dismissesPromotion_callsShowFreemium_andFiresPixel() async throws { // Given - mockUserStateManager.didActivate = false + try await waitForViewModelUpdate { + mockUserStateManager.didActivate = false + sut.isHomePagePromotionVisible = true + } // When - let viewModel = sut.viewModel - viewModel!.proceedAction() + let viewModel = try XCTUnwrap(sut.viewModel) + await viewModel.proceedAction() // Then XCTAssertTrue(mockUserStateManager.didDismissHomePagePromotion) @@ -107,10 +110,11 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testCloseAction_dismissesPromotion_andFiresPixel() { + func testCloseAction_dismissesPromotion_andFiresPixel() async throws { // When - let viewModel = sut.viewModel - viewModel!.closeAction() + try await waitForViewModelUpdate() + let viewModel = try XCTUnwrap(sut.viewModel) + viewModel.closeAction() // Then XCTAssertTrue(mockUserStateManager.didDismissHomePagePromotion) @@ -118,14 +122,16 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testProceedAction_dismissesResults_callsShowFreemium_andFiresPixel() { + func testProceedAction_dismissesResults_callsShowFreemium_andFiresPixel() async throws { // Given - mockUserStateManager.didActivate = false - mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 5, brokerCount: 2) + try await waitForViewModelUpdate { + mockUserStateManager.didActivate = false + mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 5, brokerCount: 2) + } // When - let viewModel = sut.viewModel - viewModel!.proceedAction() + let viewModel = try XCTUnwrap(sut.viewModel) + await viewModel.proceedAction() // Then XCTAssertTrue(mockUserStateManager.didDismissHomePagePromotion) @@ -134,13 +140,15 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testCloseAction_dismissesResults_andFiresPixel() { + func testCloseAction_dismissesResults_andFiresPixel() async throws { // Given - mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 5, brokerCount: 2) + try await waitForViewModelUpdate { + mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 5, brokerCount: 2) + } // When - let viewModel = sut.viewModel - viewModel!.closeAction() + let viewModel = try XCTUnwrap(sut.viewModel) + viewModel.closeAction() // Then XCTAssertTrue(mockUserStateManager.didDismissHomePagePromotion) @@ -148,14 +156,16 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testProceedAction_dismissesNoResults_callsShowFreemium_andFiresPixel() { + func testProceedAction_dismissesNoResults_callsShowFreemium_andFiresPixel() async throws { // Given - mockUserStateManager.didActivate = false - mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 0, brokerCount: 0) + try await waitForViewModelUpdate { + mockUserStateManager.didActivate = false + mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 0, brokerCount: 0) + } // When - let viewModel = sut.viewModel - viewModel!.proceedAction() + let viewModel = try XCTUnwrap(sut.viewModel) + await viewModel.proceedAction() // Then XCTAssertTrue(mockUserStateManager.didDismissHomePagePromotion) @@ -164,13 +174,15 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testCloseAction_dismissesNoResults_andFiresPixel() { + func testCloseAction_dismissesNoResults_andFiresPixel() async throws { // Given - mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 0, brokerCount: 0) + try await waitForViewModelUpdate { + mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 0, brokerCount: 0) + } // When - let viewModel = sut.viewModel - viewModel!.closeAction() + let viewModel = try XCTUnwrap(sut.viewModel) + viewModel.closeAction() // Then XCTAssertTrue(mockUserStateManager.didDismissHomePagePromotion) @@ -178,33 +190,36 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { } @MainActor - func testViewModel_whenResultsExist_withMatches() { + func testViewModel_whenResultsExist_withMatches() async throws { // Given - mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 5, brokerCount: 2) + try await waitForViewModelUpdate { + mockUserStateManager.firstScanResults = FreemiumDBPMatchResults(matchesCount: 5, brokerCount: 2) + } // When - let viewModel = sut.viewModel + let viewModel = try await waitForViewModelUpdate() // Then - XCTAssertEqual(viewModel!.description, UserText.homePagePromotionFreemiumDBPPostScanEngagementResultPluralDescription(resultCount: 5, brokerCount: 2)) + XCTAssertEqual(viewModel?.description, UserText.homePagePromotionFreemiumDBPPostScanEngagementResultPluralDescription(resultCount: 5, brokerCount: 2)) } @MainActor - func testViewModel_whenNoResultsExist() { + func testViewModel_whenNoResultsExist() async throws { // Given - mockUserStateManager.firstScanResults = nil - - // When - let viewModel = sut.viewModel + let viewModel = try await waitForViewModelUpdate { + mockUserStateManager.firstScanResults = nil + } // Then - XCTAssertEqual(viewModel!.description, UserText.homePagePromotionFreemiumDBPDescriptionMarkdown) + XCTAssertEqual(viewModel?.description, UserText.homePagePromotionFreemiumDBPDescriptionMarkdown) } @MainActor - func testViewModel_whenFeatureNotEnabled() { + func testViewModel_whenFeatureNotEnabled() async throws { // Given - mockFeature.featureAvailable = false + try await waitForViewModelUpdate { + mockFeature.featureAvailable = false + } // When let viewModel = sut.viewModel @@ -346,6 +361,27 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { // Then XCTAssertFalse(sut.isHomePagePromotionVisible) } + + // MARK: - Helpers + + /** + * Sets up an expectation, then sets up Combine subscription for `sut.$viewModel` that fulfills the expectation, + * then calls the provided `block`, enables home page promotion and waits for time specified by `duration` + * before cancelling the subscription. + */ + @discardableResult + private func waitForViewModelUpdate(for duration: TimeInterval = 1, _ block: () async -> Void = {}) async throws -> PromotionViewModel? { + let expectation = self.expectation(description: "viewModelUpdate") + let cancellable = sut.$viewModel.dropFirst().prefix(1).sink { _ in expectation.fulfill() } + + await block() + sut.isHomePagePromotionVisible = true + + await fulfillment(of: [expectation], timeout: duration) + cancellable.cancel() + + return sut.viewModel + } } class MockFreemiumDBPExperimentPixelHandler: EventMapping { diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index f25dcdd5ae..3dffb7705c 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -358,7 +358,11 @@ final class NetworkProtectionVisibilityMock: VPNFeatureGatekeeper { } final class MockFreemiumDBPFeature: FreemiumDBPFeature { - var featureAvailable = false + var featureAvailable = false { + didSet { + isAvailableSubject.send(featureAvailable) + } + } var isAvailableSubject = PassthroughSubject() var isAvailable: Bool { From 854931c7211429fdf974d28f1c39ed9c5852ddd3 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 09:27:10 +0100 Subject: [PATCH 53/62] Refactor unit tests to extract MessageHelper into a separate file --- .../Helpers/MessageHelper.swift | 57 ++++++++++ .../Helpers/NewTabPageTestsHelper.swift | 35 ------ .../Mocks/RemoteMessageModelMocks.swift | 75 +++++++++++++ .../NewTabPageConfigurationClientTests.swift | 26 ++--- ...ewTabPageCustomBackgroundClientTests.swift | 29 ++--- .../NewTabPageFavoritesClientTests.swift | 42 +++----- .../NewTabPageFreemiumDBPClientTests.swift | 24 ++--- .../NewTabPageNextStepsCardsClientTests.swift | 75 ++++++------- .../NewTabPagePrivacyStatsClientTests.swift | 33 ++---- .../NewTabPageRMFClientTests.swift | 102 +++--------------- 10 files changed, 224 insertions(+), 274 deletions(-) create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/MessageHelper.swift delete mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/NewTabPageTestsHelper.swift create mode 100644 LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/RemoteMessageModelMocks.swift diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/MessageHelper.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/MessageHelper.swift new file mode 100644 index 0000000000..5d2996ec48 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/MessageHelper.swift @@ -0,0 +1,57 @@ +// +// MessageHelper.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 NewTabPage +import XCTest + +final class MessageHelper where MessageName.RawValue == String { + let userScript: NewTabPageUserScript + + init(userScript: NewTabPageUserScript) { + self.userScript = userScript + } + + func handleMessage(named methodName: MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(Self.asJSON(parameters), .init()) + return try XCTUnwrap(response as? Response, file: file, line: line) + } + + func handleMessageIgnoringResponse(named methodName: MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + _ = try await handler(Self.asJSON(parameters), .init()) + } + + func handleMessageExpectingNilResponse(named methodName: MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { + let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) + let response = try await handler(Self.asJSON(parameters), .init()) + XCTAssertNil(response, file: file, line: line) + } + + private static func asJSON(_ value: Any, file: StaticString = #file, line: UInt = #line) throws -> Any { + if JSONSerialization.isValidJSONObject(value) { + return value + } + if let encodableValue = value as? Encodable { + let jsonData = try JSONEncoder().encode(encodableValue) + return try JSONSerialization.jsonObject(with: jsonData) + } + XCTFail("invalid JSON value", file: file, line: line) + return [] + } +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/NewTabPageTestsHelper.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/NewTabPageTestsHelper.swift deleted file mode 100644 index abf110eb36..0000000000 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Helpers/NewTabPageTestsHelper.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// NewTabPageTestsHelper.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 XCTest - -enum NewTabPageTestsHelper { - - static func asJSON(_ value: Any, file: StaticString = #file, line: UInt = #line) throws -> Any { - if JSONSerialization.isValidJSONObject(value) { - return value - } - if let encodableValue = value as? Encodable { - let jsonData = try JSONEncoder().encode(encodableValue) - return try JSONSerialization.jsonObject(with: jsonData) - } - XCTFail("invalid JSON value", file: file, line: line) - return [] - } -} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/RemoteMessageModelMocks.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/RemoteMessageModelMocks.swift new file mode 100644 index 0000000000..66bd9346f3 --- /dev/null +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/Mocks/RemoteMessageModelMocks.swift @@ -0,0 +1,75 @@ +// +// RemoteMessageModelMocks.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import RemoteMessaging + +extension RemoteMessageModel { + static func mockSmall(id: String) -> RemoteMessageModel { + .init( + id: id, + content: .small(titleText: "title", descriptionText: "description"), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: true + ) + } + + static func mockMedium(id: String) -> RemoteMessageModel { + .init( + id: "sample_message", + content: .medium(titleText: "title", descriptionText: "description", placeholder: .criticalUpdate), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: true + ) + } + + static func mockBigSingleAction(id: String, action: RemoteAction) -> RemoteMessageModel { + .init( + id: "sample_message", + content: .bigSingleAction( + titleText: "title", + descriptionText: "description", + placeholder: .ddgAnnounce, + primaryActionText: "primary_action", + primaryAction: action + ), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: true + ) + } + + static func mockBigTwoAction(id: String, primaryAction: RemoteAction, secondaryAction: RemoteAction) -> RemoteMessageModel { + .init( + id: "sample_message", + content: .bigTwoAction( + titleText: "title", + descriptionText: "description", + placeholder: .ddgAnnounce, + primaryActionText: "primary_action", + primaryAction: primaryAction, + secondaryActionText: "secondary_action", + secondaryAction: secondaryAction + ), + matchingRules: [], + exclusionRules: [], + isMetricsEnabled: true + ) + } +} diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift index 5460508f63..4b61d1f2f1 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageConfigurationClientTests.swift @@ -26,6 +26,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { private var sectionsVisibilityProvider: MockNewTabPageSectionsVisibilityProvider! private var contextMenuPresenter: CapturingNewTabPageContextMenuPresenter! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! override func setUpWithError() throws { try super.setUpWithError() @@ -39,6 +40,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { ) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } @@ -53,7 +55,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { .init(id: .favorites, title: "Favorites"), .init(id: .privacyStats, title: "Privacy Stats") ]) - try await sendMessageExpectingNilResponse(named: .contextMenu, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .contextMenu, parameters: parameters) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 1) let menu = try XCTUnwrap(contextMenuPresenter.showContextMenuCalls.first) @@ -66,7 +68,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { func testWhenContextMenuParamsIsEmptyThenContextMenuDoesNotShow() async throws { let parameters = NewTabPageDataModel.ContextMenuParams(visibilityMenuItems: []) - try await sendMessageExpectingNilResponse(named: .contextMenu, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .contextMenu, parameters: parameters) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 0) } @@ -74,7 +76,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { // MARK: - initialSetup func testThatInitialSetupReturnsConfiguration() async throws { - let configuration: NewTabPageDataModel.NewTabPageConfiguration = try await sendMessage(named: .initialSetup) + let configuration: NewTabPageDataModel.NewTabPageConfiguration = try await messageHelper.handleMessage(named: .initialSetup) XCTAssertEqual(configuration.widgets, [ .init(id: .rmf), .init(id: .freemiumPIRBanner), @@ -96,7 +98,7 @@ final class NewTabPageConfigurationClientTests: XCTestCase { .init(id: .favorites, isVisible: false), .init(id: .privacyStats, isVisible: true) ] - try await sendMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) + try await messageHelper.handleMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) XCTAssertEqual(sectionsVisibilityProvider.isFavoritesVisible, false) XCTAssertEqual(sectionsVisibilityProvider.isPrivacyStatsVisible, true) } @@ -107,22 +109,8 @@ final class NewTabPageConfigurationClientTests: XCTestCase { let configs: [NewTabPageDataModel.NewTabPageConfiguration.WidgetConfig] = [ .init(id: .privacyStats, isVisible: false) ] - try await sendMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) + try await messageHelper.handleMessageExpectingNilResponse(named: .widgetsSetConfig, parameters: configs) XCTAssertEqual(sectionsVisibilityProvider.isFavoritesVisible, initialIsFavoritesVisible) XCTAssertEqual(sectionsVisibilityProvider.isPrivacyStatsVisible, false) } - - // MARK: - Helper functions - - func sendMessage(named methodName: NewTabPageConfigurationClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func sendMessageExpectingNilResponse(named methodName: NewTabPageConfigurationClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift index 5ea809676b..960a2c156a 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageCustomBackgroundClientTests.swift @@ -25,6 +25,7 @@ final class NewTabPageCustomBackgroundClientTests: XCTestCase { private var client: NewTabPageCustomBackgroundClient! private var model: CapturingNewTabPageCustomBackgroundProvider! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! override func setUpWithError() throws { try super.setUpWithError() @@ -32,6 +33,7 @@ final class NewTabPageCustomBackgroundClientTests: XCTestCase { client = NewTabPageCustomBackgroundClient(model: model) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } @@ -39,7 +41,7 @@ final class NewTabPageCustomBackgroundClientTests: XCTestCase { func testThatDeleteImageCallsModel() async throws { let deleteData = NewTabPageDataModel.DeleteImageData(id: "abcd") - try await handleMessageExpectingNilResponse(named: .deleteImage, parameters: deleteData) + try await messageHelper.handleMessageExpectingNilResponse(named: .deleteImage, parameters: deleteData) XCTAssertEqual(model.deleteImageCalls, ["abcd"]) } @@ -47,7 +49,7 @@ final class NewTabPageCustomBackgroundClientTests: XCTestCase { func testThatSetBackgroundCallsModel() async throws { let backgroundData = NewTabPageDataModel.BackgroundData(background: .gradient("gradient01")) - try await handleMessageExpectingNilResponse(named: .setBackground, parameters: backgroundData) + try await messageHelper.handleMessageExpectingNilResponse(named: .setBackground, parameters: backgroundData) XCTAssertEqual(model.background, .gradient("gradient01")) } @@ -55,33 +57,14 @@ final class NewTabPageCustomBackgroundClientTests: XCTestCase { func testThatSetThemeCallsModel() async throws { let themeData = NewTabPageDataModel.ThemeData(theme: .dark) - try await handleMessageExpectingNilResponse(named: .setTheme, parameters: themeData) + try await messageHelper.handleMessageExpectingNilResponse(named: .setTheme, parameters: themeData) XCTAssertEqual(model.theme, .dark) } // MARK: - upload func testThatUploadCallsModel() async throws { - try await handleMessageExpectingNilResponse(named: .upload) + try await messageHelper.handleMessageExpectingNilResponse(named: .upload) XCTAssertEqual(model.presentUploadDialogCallsCount, 1) } - - // MARK: - Helper functions - - func handleMessage(named methodName: NewTabPageCustomBackgroundClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func handleMessageIgnoringResponse(named methodName: NewTabPageCustomBackgroundClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - } - - func handleMessageExpectingNilResponse(named methodName: NewTabPageCustomBackgroundClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift index b49e9e63a3..b638bc2ba8 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFavoritesClientTests.swift @@ -30,6 +30,7 @@ final class NewTabPageFavoritesClientTests: XCTestCase { private var actionsHandler: CapturingNewTabPageFavoritesActionsHandler! private var favoritesModel: NewTabPageFavoritesModel! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! @MainActor override func setUpWithError() throws { @@ -46,13 +47,14 @@ final class NewTabPageFavoritesClientTests: XCTestCase { client = NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: 100) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } // MARK: - add func testThatAddCallsAddAction() async throws { - try await handleMessageExpectingNilResponse(named: .add) + try await messageHelper.handleMessageExpectingNilResponse(named: .add) XCTAssertEqual(actionsHandler.addNewFavoriteCallCount, 1) } @@ -60,14 +62,14 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testWhenFavoritesViewIsExpandedThenGetConfigReturnsExpandedState() async throws { favoritesModel.isViewExpanded = true - let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + let config: NewTabPageUserScript.WidgetConfig = try await messageHelper.handleMessage(named: .getConfig) XCTAssertEqual(config.animation, .auto) XCTAssertEqual(config.expansion, .expanded) } func testWhenFavoritesViewIsCollapsedThenGetConfigReturnsCollapsedState() async throws { favoritesModel.isViewExpanded = false - let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + let config: NewTabPageUserScript.WidgetConfig = try await messageHelper.handleMessage(named: .getConfig) XCTAssertEqual(config.animation, .auto) XCTAssertEqual(config.expansion, .collapsed) } @@ -77,14 +79,14 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testWhenSetConfigContainsExpandedStateThenFavoritesModelSettingIsSetToExpanded() async throws { favoritesModel.isViewExpanded = false let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .expanded) - try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + try await messageHelper.handleMessageExpectingNilResponse(named: .setConfig, parameters: config) XCTAssertEqual(favoritesModel.isViewExpanded, true) } func testWhenSetConfigContainsCollapsedStateThenFavoritesModelSettingIsSetToCollapsed() async throws { favoritesModel.isViewExpanded = true let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .collapsed) - try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + try await messageHelper.handleMessageExpectingNilResponse(named: .setConfig, parameters: config) XCTAssertEqual(favoritesModel.isViewExpanded, false) } @@ -98,7 +100,7 @@ final class NewTabPageFavoritesClientTests: XCTestCase { MockNewTabPageFavorite(id: "2", title: "D", url: "https://d.com"), MockNewTabPageFavorite(id: "3", title: "E", url: "https://e.com") ] - let data: NewTabPageDataModel.FavoritesData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.FavoritesData = try await messageHelper.handleMessage(named: .getData) XCTAssertEqual(data.favorites, [ .init(id: "1", title: "A", url: "https://a.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//a.com")), .init(id: "10", title: "B", url: "https://b.com", favicon: .init(maxAvailableSize: 100, src: "duck://favicon/https%3A//b.com")), @@ -110,7 +112,7 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testWhenFavoritesAreEmptyThenGetDataReturnsNoFavorites() async throws { favoritesModel.favorites = [] - let data: NewTabPageDataModel.FavoritesData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.FavoritesData = try await messageHelper.handleMessage(named: .getData) XCTAssertEqual(data.favorites, []) } @@ -118,13 +120,13 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testThatMoveActionIsForwardedToTheModel() async throws { let action = NewTabPageDataModel.FavoritesMoveAction(id: "abcd", fromIndex: 10, targetIndex: 4) - try await handleMessageExpectingNilResponse(named: .move, parameters: action) + try await messageHelper.handleMessageExpectingNilResponse(named: .move, parameters: action) XCTAssertEqual(actionsHandler.moveCalls, [.init("abcd", 4)]) } func testThatWhenFavoriteIsMovedToHigherIndexThenModelIncrementsIndex() async throws { let action = NewTabPageDataModel.FavoritesMoveAction(id: "abcd", fromIndex: 1, targetIndex: 4) - try await handleMessageExpectingNilResponse(named: .move, parameters: action) + try await messageHelper.handleMessageExpectingNilResponse(named: .move, parameters: action) XCTAssertEqual(actionsHandler.moveCalls, [.init("abcd", 5)]) } @@ -132,13 +134,13 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testThatOpenActionIsForwardedToTheModel() async throws { let action = NewTabPageDataModel.FavoritesOpenAction(id: "abcd", url: "https://example.com") - try await handleMessageExpectingNilResponse(named: .open, parameters: action) + try await messageHelper.handleMessageExpectingNilResponse(named: .open, parameters: action) XCTAssertEqual(actionsHandler.openCalls, [.init(URL(string: "https://example.com")!, .current)]) } func testWhenURLIsInvalidThenOpenActionIsNotForwardedToTheModel() async throws { let action = NewTabPageDataModel.FavoritesOpenAction(id: "abcd", url: "abcd") - try await handleMessageExpectingNilResponse(named: .open, parameters: action) + try await messageHelper.handleMessageExpectingNilResponse(named: .open, parameters: action) XCTAssertEqual(actionsHandler.openCalls, []) } @@ -147,28 +149,14 @@ final class NewTabPageFavoritesClientTests: XCTestCase { func testThatOpenContextMenuActionForExistingFavoriteIsForwardedToTheModel() async throws { favoritesModel.favorites = [.init(id: "abcd", title: "A", url: "https://example.com")] let action = NewTabPageDataModel.FavoritesContextMenuAction(id: "abcd") - try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) + try await messageHelper.handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 1) } func testThatOpenContextMenuActionForNotExistingFavoriteIsNotForwardedToTheModel() async throws { favoritesModel.favorites = [] let action = NewTabPageDataModel.FavoritesContextMenuAction(id: "abcd") - try await handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) + try await messageHelper.handleMessageExpectingNilResponse(named: .openContextMenu, parameters: action) XCTAssertEqual(contextMenuPresenter.showContextMenuCalls.count, 0) } - - // MARK: - Helper functions - - func handleMessage(named methodName: NewTabPageFavoritesClientUnderTest.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func handleMessageExpectingNilResponse(named methodName: NewTabPageFavoritesClientUnderTest.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift index 81a1443ed9..610ee9932f 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageFreemiumDBPClientTests.swift @@ -25,25 +25,27 @@ final class NewTabPageFreemiumDBPClientTests: XCTestCase { private var client: NewTabPageFreemiumDBPClient! private var provider: CapturingNewTabPageFreemiumDBPBannerProvider! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! override func setUpWithError() throws { try super.setUpWithError() provider = CapturingNewTabPageFreemiumDBPBannerProvider() client = NewTabPageFreemiumDBPClient(provider: provider) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } // MARK: - getData func testWhenMessageIsNilThenGetDataReturnsNilMessage() async throws { - let messageData: NewTabPageDataModel.FreemiumPIRBannerMessageData = try await sendMessage(named: .getData) + let messageData: NewTabPageDataModel.FreemiumPIRBannerMessageData = try await messageHelper.handleMessage(named: .getData) XCTAssertNil(messageData.content) } func testThatGetDataReturnsMessageIfPresent() async throws { provider.bannerMessage = .init(titleText: "sample_title", descriptionText: "sample_description", actionText: "sample_action") - let messageData: NewTabPageDataModel.FreemiumPIRBannerMessageData = try await sendMessage(named: .getData) + let messageData: NewTabPageDataModel.FreemiumPIRBannerMessageData = try await messageHelper.handleMessage(named: .getData) let message = try XCTUnwrap(messageData.content) XCTAssertEqual(message, .init(titleText: "sample_title", descriptionText: "sample_description", actionText: "sample_action")) } @@ -51,28 +53,14 @@ final class NewTabPageFreemiumDBPClientTests: XCTestCase { // MARK: - dismiss func testThatDismissIsForwardedToProvider() async throws { - try await sendMessageExpectingNilResponse(named: .dismiss) + try await messageHelper.handleMessageExpectingNilResponse(named: .dismiss) XCTAssertEqual(provider.dismissCallCount, 1) } // MARK: - action func testThatActionIsForwardedToProvider() async throws { - try await sendMessageExpectingNilResponse(named: .action) + try await messageHelper.handleMessageExpectingNilResponse(named: .action) XCTAssertEqual(provider.actionCallCount, 1) } - - // MARK: - Helper functions - - func sendMessage(named methodName: NewTabPageFreemiumDBPClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func sendMessageExpectingNilResponse(named methodName: NewTabPageFreemiumDBPClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift index c3605441cb..d863006a09 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageNextStepsCardsClientTests.swift @@ -25,6 +25,7 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { private var client: NewTabPageNextStepsCardsClient! private var model: CapturingNewTabPageNextStepsCardsProvider! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! @MainActor override func setUpWithError() throws { @@ -33,24 +34,25 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { client = NewTabPageNextStepsCardsClient(model: model) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } // MARK: - action func testThatActionCallsHandleAction() async throws { - try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .defaultApp)) - try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .duckplayer)) - try await handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .bringStuff)) + try await messageHelper.handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .defaultApp)) + try await messageHelper.handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .duckplayer)) + try await messageHelper.handleMessageExpectingNilResponse(named: .action, parameters: NewTabPageDataModel.Card(id: .bringStuff)) XCTAssertEqual(model.handleActionCalls, [.defaultApp, .duckplayer, .bringStuff]) } // MARK: - dismiss func testThatDismissCallsDismissHandler() async throws { - try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .defaultApp)) - try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .duckplayer)) - try await handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .bringStuff)) + try await messageHelper.handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .defaultApp)) + try await messageHelper.handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .duckplayer)) + try await messageHelper.handleMessageExpectingNilResponse(named: .dismiss, parameters: NewTabPageDataModel.Card(id: .bringStuff)) XCTAssertEqual(model.dismissCalls, [.defaultApp, .duckplayer, .bringStuff]) } @@ -58,14 +60,14 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { func testWhenNextStepsViewIsExpandedThenGetConfigReturnsExpandedState() async throws { model.isViewExpanded = true - let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + let config: NewTabPageUserScript.WidgetConfig = try await messageHelper.handleMessage(named: .getConfig) XCTAssertEqual(config.animation, .auto) XCTAssertEqual(config.expansion, .expanded) } func testWhenNextStepsViewIsCollapsedThenGetConfigReturnsCollapsedState() async throws { model.isViewExpanded = false - let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + let config: NewTabPageUserScript.WidgetConfig = try await messageHelper.handleMessage(named: .getConfig) XCTAssertEqual(config.animation, .auto) XCTAssertEqual(config.expansion, .collapsed) } @@ -75,14 +77,14 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { func testWhenSetConfigContainsExpandedStateThenModelSettingIsSetToExpanded() async throws { model.isViewExpanded = false let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .expanded) - try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + try await messageHelper.handleMessageExpectingNilResponse(named: .setConfig, parameters: config) XCTAssertEqual(model.isViewExpanded, true) } func testWhenSetConfigContainsCollapsedStateThenModelSettingIsSetToCollapsed() async throws { model.isViewExpanded = true let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .collapsed) - try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + try await messageHelper.handleMessageExpectingNilResponse(named: .setConfig, parameters: config) XCTAssertEqual(model.isViewExpanded, false) } @@ -94,7 +96,7 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { .duckplayer, .bringStuff ] - let data: NewTabPageDataModel.NextStepsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.NextStepsData = try await messageHelper.handleMessage(named: .getData) XCTAssertEqual(data, .init(content: [ .init(id: .addAppToDockMac), .init(id: .duckplayer), @@ -104,7 +106,7 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { func testWhenCardsAreEmptyThenGetDataReturnsNilContent() async throws { model.cards = [] - let data: NewTabPageDataModel.NextStepsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.NextStepsData = try await messageHelper.handleMessage(named: .getData) XCTAssertEqual(data, .init(content: nil)) } @@ -114,8 +116,8 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { model.cards = [.addAppToDockMac, .duckplayer] try await performAndWaitForWillDisplayCards { - try await handleMessageIgnoringResponse(named: .getData) - try await handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) } XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) @@ -125,17 +127,17 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { model.cards = [.addAppToDockMac, .duckplayer] try await performAndWaitForWillDisplayCards(count: 0, timeout: 0.1) { - try await handleMessageIgnoringResponse(named: .getData) - try await handleMessageIgnoringResponse(named: .getData) - try await handleMessageIgnoringResponse(named: .getData) - try await handleMessageIgnoringResponse(named: .getData) - try await handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) } XCTAssertEqual(model.willDisplayCardsCalls, []) try await performAndWaitForWillDisplayCards { - try await handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) } XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) @@ -145,17 +147,17 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { model.cards = [.addAppToDockMac, .duckplayer] try await performAndWaitForWillDisplayCards(count: 0, timeout: 0.1) { - try await handleMessageIgnoringResponse(named: .getConfig) - try await handleMessageIgnoringResponse(named: .getConfig) - try await handleMessageIgnoringResponse(named: .getConfig) - try await handleMessageIgnoringResponse(named: .getConfig) - try await handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) } XCTAssertEqual(model.willDisplayCardsCalls, []) try await performAndWaitForWillDisplayCards { - try await handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) } XCTAssertEqual(model.willDisplayCardsCalls, [[.addAppToDockMac, .duckplayer]]) @@ -248,8 +250,8 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { func triggerInitialCardsEventAndResetMockState() async throws { try await performAndWaitForWillDisplayCards { - try await handleMessageIgnoringResponse(named: .getConfig) - try await handleMessageIgnoringResponse(named: .getData) + try await messageHelper.handleMessageIgnoringResponse(named: .getConfig) + try await messageHelper.handleMessageIgnoringResponse(named: .getData) } model.willDisplayCardsCalls = [] } @@ -271,21 +273,4 @@ final class NewTabPageNextStepsCardsClientTests: XCTestCase { model.willDisplayCardsImpl = originalImpl } - - func handleMessage(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func handleMessageIgnoringResponse(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - } - - func handleMessageExpectingNilResponse(named methodName: NewTabPageNextStepsCardsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift index d5edc36305..767fc19c4e 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPagePrivacyStatsClientTests.swift @@ -33,6 +33,7 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { private var settingsPersistor: UserDefaultsNewTabPagePrivacyStatsSettingsPersistor! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! override func setUp() async throws { try await super.setUp() @@ -52,6 +53,7 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { client = NewTabPagePrivacyStatsClient(model: model) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } @@ -59,14 +61,14 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { func testWhenPrivacyStatsViewIsExpandedThenGetConfigReturnsExpandedState() async throws { model.isViewExpanded = true - let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + let config: NewTabPageUserScript.WidgetConfig = try await messageHelper.handleMessage(named: .getConfig) XCTAssertEqual(config.animation, .auto) XCTAssertEqual(config.expansion, .expanded) } func testWhenPrivacyStatsViewIsCollapsedThenGetConfigReturnsCollapsedState() async throws { model.isViewExpanded = false - let config: NewTabPageUserScript.WidgetConfig = try await handleMessage(named: .getConfig) + let config: NewTabPageUserScript.WidgetConfig = try await messageHelper.handleMessage(named: .getConfig) XCTAssertEqual(config.animation, .auto) XCTAssertEqual(config.expansion, .collapsed) } @@ -76,14 +78,14 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { func testWhenSetConfigContainsExpandedStateThenModelSettingIsSetToExpanded() async throws { model.isViewExpanded = false let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .expanded) - try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + try await messageHelper.handleMessageExpectingNilResponse(named: .setConfig, parameters: config) XCTAssertEqual(model.isViewExpanded, true) } func testWhenSetConfigContainsCollapsedStateThenModelSettingIsSetToCollapsed() async throws { model.isViewExpanded = true let config = NewTabPageUserScript.WidgetConfig(animation: .auto, expansion: .collapsed) - try await handleMessageExpectingNilResponse(named: .setConfig, parameters: config) + try await messageHelper.handleMessageExpectingNilResponse(named: .setConfig, parameters: config) XCTAssertEqual(model.isViewExpanded, false) } @@ -110,9 +112,10 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { client = NewTabPagePrivacyStatsClient(model: model) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) - let data: NewTabPageDataModel.PrivacyStatsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.PrivacyStatsData = try await messageHelper.handleMessage(named: .getData) XCTAssertEqual(data, .init(totalCount: 2510, trackerCompanies: [ .init(count: 1, displayName: "A"), .init(count: 2, displayName: "B"), @@ -123,35 +126,21 @@ final class NewTabPagePrivacyStatsClientTests: XCTestCase { } func testWhenPrivacyStatsAreEmptyThenGetDataReturnsEmptyArray() async throws { - let data: NewTabPageDataModel.PrivacyStatsData = try await handleMessage(named: .getData) + let data: NewTabPageDataModel.PrivacyStatsData = try await messageHelper.handleMessage(named: .getData) XCTAssertEqual(data, .init(totalCount: 0, trackerCompanies: [])) } // MARK: - showLess func testThatShowLessIsPassedToTheModelAndToTheEventMapping() async throws { - try await handleMessageExpectingNilResponse(named: .showLess) + try await messageHelper.handleMessageExpectingNilResponse(named: .showLess) XCTAssertEqual(eventMapping.events, [.showLess]) } // MARK: - showMore func testThatShowMoreIsPassedToTheModelAndToTheEventMapping() async throws { - try await handleMessageExpectingNilResponse(named: .showMore) + try await messageHelper.handleMessageExpectingNilResponse(named: .showMore) XCTAssertEqual(eventMapping.events, [.showMore]) } - - // MARK: - Helper functions - - func handleMessage(named methodName: NewTabPagePrivacyStatsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func handleMessageExpectingNilResponse(named methodName: NewTabPagePrivacyStatsClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } } diff --git a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift index 1d6ccfee56..5fb46a8f69 100644 --- a/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift +++ b/LocalPackages/NewTabPage/Tests/NewTabPageTests/NewTabPageRMFClientTests.swift @@ -25,17 +25,19 @@ final class NewTabPageRMFClientTests: XCTestCase { private var client: NewTabPageRMFClient! private var remoteMessageProvider: CapturingNewTabPageActiveRemoteMessageProvider! private var userScript: NewTabPageUserScript! + private var messageHelper: MessageHelper! override func setUpWithError() throws { try super.setUpWithError() remoteMessageProvider = CapturingNewTabPageActiveRemoteMessageProvider() client = NewTabPageRMFClient(remoteMessageProvider: remoteMessageProvider) userScript = NewTabPageUserScript() + messageHelper = .init(userScript: userScript) client.registerMessageHandlers(for: userScript) } func testWhenMessageIsNilThenGetDataReturnsNilMessage() async throws { - let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await messageHelper.handleMessage(named: .rmfGetData) XCTAssertNil(rmfData.content) } @@ -43,21 +45,21 @@ final class NewTabPageRMFClientTests: XCTestCase { func testThatGetDataReturnsSmallMessageIfPresent() async throws { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") - let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await messageHelper.handleMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .small(.init(id: "sample_message", titleText: "title", descriptionText: "description"))) } func testThatGetDataReturnsMediumMessageIfPresent() async throws { remoteMessageProvider.newTabPageRemoteMessage = .mockMedium(id: "sample_message") - let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await messageHelper.handleMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .medium(.init(id: "sample_message", titleText: "title", descriptionText: "description", icon: .criticalUpdate))) } func testThatGetDataReturnsBigSingleActionMessageIfPresent() async throws { remoteMessageProvider.newTabPageRemoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) - let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await messageHelper.handleMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .bigSingleAction( .init( @@ -72,7 +74,7 @@ final class NewTabPageRMFClientTests: XCTestCase { func testThatGetDataReturnsBigTwoActionMessageIfPresent() async throws { remoteMessageProvider.newTabPageRemoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) - let rmfData: NewTabPageDataModel.RMFData = try await sendMessage(named: .rmfGetData) + let rmfData: NewTabPageDataModel.RMFData = try await messageHelper.handleMessage(named: .rmfGetData) let message = try XCTUnwrap(rmfData.content) XCTAssertEqual(message, .bigTwoAction( .init( @@ -92,7 +94,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: nil, button: .close)]) } @@ -100,7 +102,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageDataModel.RemoteMessageParams(id: "different_sample_message") - try await sendMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfDismiss, parameters: parameters) XCTAssertTrue(remoteMessageProvider.dismissCalls.isEmpty) } @@ -110,7 +112,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .action)]) } @@ -118,7 +120,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .appStore, secondaryAction: .dismiss) let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .primaryAction)]) } @@ -126,7 +128,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -134,7 +136,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageDataModel.RemoteMessageParams(id: "different_sample_message") - try await sendMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfPrimaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -144,7 +146,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockBigTwoAction(id: "sample_message", primaryAction: .dismiss, secondaryAction: .appStore) let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, [.init(action: .appStore, button: .secondaryAction)]) } @@ -152,7 +154,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockBigSingleAction(id: "sample_message", action: .appStore) let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -160,7 +162,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageDataModel.RemoteMessageParams(id: "sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } @@ -168,77 +170,7 @@ final class NewTabPageRMFClientTests: XCTestCase { remoteMessageProvider.newTabPageRemoteMessage = .mockSmall(id: "sample_message") let parameters = NewTabPageDataModel.RemoteMessageParams(id: "different_sample_message") - try await sendMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) + try await messageHelper.handleMessageExpectingNilResponse(named: .rmfSecondaryAction, parameters: parameters) XCTAssertEqual(remoteMessageProvider.dismissCalls, []) } - - // MARK: - Helper functions - - func sendMessage(named methodName: NewTabPageRMFClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws -> Response { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - return try XCTUnwrap(response as? Response, file: file, line: line) - } - - func sendMessageExpectingNilResponse(named methodName: NewTabPageRMFClient.MessageName, parameters: Any = [], file: StaticString = #file, line: UInt = #line) async throws { - let handler = try XCTUnwrap(userScript.handler(forMethodNamed: methodName.rawValue), file: file, line: line) - let response = try await handler(NewTabPageTestsHelper.asJSON(parameters), .init()) - XCTAssertNil(response, file: file, line: line) - } -} - -fileprivate extension RemoteMessageModel { - static func mockSmall(id: String) -> RemoteMessageModel { - .init( - id: id, - content: .small(titleText: "title", descriptionText: "description"), - matchingRules: [], - exclusionRules: [], - isMetricsEnabled: true - ) - } - - static func mockMedium(id: String) -> RemoteMessageModel { - .init( - id: "sample_message", - content: .medium(titleText: "title", descriptionText: "description", placeholder: .criticalUpdate), - matchingRules: [], - exclusionRules: [], - isMetricsEnabled: true - ) - } - - static func mockBigSingleAction(id: String, action: RemoteAction) -> RemoteMessageModel { - .init( - id: "sample_message", - content: .bigSingleAction( - titleText: "title", - descriptionText: "description", - placeholder: .ddgAnnounce, - primaryActionText: "primary_action", - primaryAction: action - ), - matchingRules: [], - exclusionRules: [], - isMetricsEnabled: true - ) - } - - static func mockBigTwoAction(id: String, primaryAction: RemoteAction, secondaryAction: RemoteAction) -> RemoteMessageModel { - .init( - id: "sample_message", - content: .bigTwoAction( - titleText: "title", - descriptionText: "description", - placeholder: .ddgAnnounce, - primaryActionText: "primary_action", - primaryAction: primaryAction, - secondaryActionText: "secondary_action", - secondaryAction: secondaryAction - ), - matchingRules: [], - exclusionRules: [], - isMetricsEnabled: true - ) - } } From 3c736c23d79200b0593d05da56598042abf6efd7 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 09:35:09 +0100 Subject: [PATCH 54/62] Update documentation for DuckURLSchemeHandler for user background images --- .../Tab/Navigation/DuckURLSchemeHandler.swift | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift index 455cb03f96..bd3a2962dc 100644 --- a/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift +++ b/DuckDuckGo/Tab/Navigation/DuckURLSchemeHandler.swift @@ -188,11 +188,13 @@ private extension DuckURLSchemeHandler { private extension DuckURLSchemeHandler { /** - * This handler supports special Duck favicon URLs and uses `FaviconManager` - * to return a favicon in response, based on the actual favicon URL that's - * encoded in the URL path. + * This handler supports Duck custom background image URL and uses `UserBackgroundImagesManager` + * to return an image in response, based on the image ID (file name) that's the last component of the URL path. + + * Custom Background image has the format of `duck://new-tab/background/images/`. + * Custom Background image thumbnail has the format of `duck://new-tab/background/thumbnails/`. * - * If favicon is not found, an `HTTP 404` response is returned. + * If an image is not found, an `HTTP 404` response is returned. */ func handleCustomBackgroundImage(urlSchemeTask: WKURLSchemeTask, isThumbnail: Bool = false) { guard let requestURL = urlSchemeTask.request.url else { @@ -200,10 +202,6 @@ private extension DuckURLSchemeHandler { return } - /** - * Custom Background image has the format of `duck://new-tab/background/images/`. - * Custom Background image thumbnail has the format of `duck://new-tab/background/thumbnails/`. - */ let fileName = requestURL.lastPathComponent guard let (response, data) = response(for: requestURL, withFileName: fileName, isThumbnail: isThumbnail) else { return } From 0598f792c85ce283014fa898d166356c869fe8db Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 11:41:45 +0100 Subject: [PATCH 55/62] Add some tests for NewTabPageCustomizationProvider --- DuckDuckGo.xcodeproj/project.pbxproj | 6 + ...NewTabPageDataModel+CustomBackground.swift | 20 +++ ...NewTabPageCustomizationProviderTests.swift | 139 ++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 01faaf39db..a65a618184 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1128,6 +1128,8 @@ 372BC2A22A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */; }; 372D15EC2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */; }; 372D15ED2D00FA1A00A11576 /* AppearancePreferences+NewTabPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */; }; + 3730F20F2D2E725D00239F96 /* NewTabPageCustomizationProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730F20E2D2E725A00239F96 /* NewTabPageCustomizationProviderTests.swift */; }; + 3730F2102D2E725D00239F96 /* NewTabPageCustomizationProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3730F20E2D2E725A00239F96 /* NewTabPageCustomizationProviderTests.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 */; }; @@ -3681,6 +3683,7 @@ 372A0FEB2B2379310033BF7F /* SyncMetricsEventsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMetricsEventsHandler.swift; sourceTree = ""; }; 372BC2A02A4AFA47001D8FD5 /* SyncCredentialsAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncCredentialsAdapter.swift; sourceTree = ""; }; 372D15EB2D00FA1400A11576 /* AppearancePreferences+NewTabPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppearancePreferences+NewTabPage.swift"; sourceTree = ""; }; + 3730F20E2D2E725A00239F96 /* NewTabPageCustomizationProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageCustomizationProviderTests.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 = ""; }; @@ -5925,6 +5928,7 @@ 377D7BC32D0AC7F100ADFD06 /* NewTabPage */ = { isa = PBXGroup; children = ( + 3730F20E2D2E725A00239F96 /* NewTabPageCustomizationProviderTests.swift */, 377D7BC02D0AC7E000ADFD06 /* NewTabPageNextStepsCardsProviderTests.swift */, ); path = NewTabPage; @@ -12554,6 +12558,7 @@ 857E5AFB2A79628A00FC0FB4 /* PixelExperimentTests.swift in Sources */, 1D638D622C44F2BA00530DD5 /* ApplicationUpdateDetectorTests.swift in Sources */, 56A054142C1C3796007D8FAB /* CapturingDockCustomizer.swift in Sources */, + 3730F20F2D2E725D00239F96 /* NewTabPageCustomizationProviderTests.swift in Sources */, 3706FE64293F661700E42796 /* DownloadListStoreTests.swift in Sources */, 3706FE65293F661700E42796 /* ContentBlockingUpdatingTests.swift in Sources */, 3706FE67293F661700E42796 /* EncryptionMocks.swift in Sources */, @@ -13994,6 +13999,7 @@ 31A2FD172BAB41C500D0E741 /* DataBrokerProtectionFeatureGatekeeperTests.swift in Sources */, 85F1B0C925EF9759004792B6 /* URLEventHandlerTests.swift in Sources */, 4B9292BD2667103100AD2C21 /* BookmarkOutlineViewDataSourceTests.swift in Sources */, + 3730F2102D2E725D00239F96 /* NewTabPageCustomizationProviderTests.swift in Sources */, C17CA7B22B9B5317008EC3C1 /* MockAutofillPopoverPresenter.swift in Sources */, 560C6ED62CCA5CE100D411E2 /* FindInView.swift in Sources */, 4BF6961D28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift in Sources */, diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift index a619f1e7c9..69ee33149e 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageDataModel+CustomBackground.swift @@ -88,6 +88,26 @@ public extension NewTabPageDataModel { case gradient(String) case userImage(UserImage) + /** + * Custom implementation of this function is here to perform case-insensitive comparison for hex colors. + */ + public static func == (lhs: Background, rhs: Background) -> Bool { + switch (lhs, rhs) { + case (.default, .default): + return true + case (.solidColor(let lColor), .solidColor(let rColor)): + return lColor == rColor + case (.hexColor(let lColor), .hexColor(let rColor)): + return lColor.lowercased() == rColor.lowercased() + case (.gradient(let lGradient), .gradient(let rGradient)): + return lGradient == rGradient + case (.userImage(let lUserImage), .userImage(let rUserImage)): + return lUserImage == rUserImage + default: + return false + } + } + enum CodingKeys: CodingKey { case kind case value diff --git a/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift b/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift new file mode 100644 index 0000000000..1b895c015f --- /dev/null +++ b/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift @@ -0,0 +1,139 @@ +// +// NewTabPageCustomizationProviderTests.swift +// +// Copyright © 2025 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import AppKitExtensions +import NewTabPage +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class NewTabPageCustomizationProviderTests: XCTestCase { + var storageLocation: URL! + var appearancePreferences: AppearancePreferences! + var userColorProvider: MockUserColorProvider! + var userBackgroundImagesManager: CapturingUserBackgroundImagesManager! + private var settingsModel: HomePage.Models.SettingsModel! + private var provider: NewTabPageCustomizationProvider! + + @MainActor + override func setUp() async throws { + + appearancePreferences = AppearancePreferences(persistor: MockAppearancePreferencesPersistor()) + storageLocation = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + userBackgroundImagesManager = CapturingUserBackgroundImagesManager(storageLocation: storageLocation, maximumNumberOfImages: 4) + userColorProvider = MockUserColorProvider() + + settingsModel = HomePage.Models.SettingsModel( + appearancePreferences: appearancePreferences, + userBackgroundImagesManager: userBackgroundImagesManager, + sendPixel: { _ in }, + openFilePanel: { nil }, + userColorProvider: self.userColorProvider, + showAddImageFailedAlert: {}, + navigator: MockHomePageSettingsModelNavigator() + ) + + provider = NewTabPageCustomizationProvider(homePageSettingsModel: settingsModel, appearancePreferences: appearancePreferences) + } + + override func tearDown() async throws { + try? FileManager.default.removeItem(at: storageLocation) + } + + func testThatCustomizerOpenerReturnsSettingsModelCustomizerOpener() { + XCTAssertIdentical(provider.customizerOpener, settingsModel.customizerOpener) + } + + func testThatBackgroundGetterReturnsSettingsModelBackground() throws { + settingsModel.customBackground = .gradient(.gradient01) + XCTAssertEqual(provider.background, .gradient("gradient01")) + + settingsModel.customBackground = .solidColor(.color02) + XCTAssertEqual(provider.background, .solidColor("color02")) + + let hexColor = try XCTUnwrap(SolidColorBackground("#abcdef")) + settingsModel.customBackground = .solidColor(hexColor) + XCTAssertEqual(provider.background, .hexColor("#abcdef")) + + let userImage = UserBackgroundImage(fileName: "abc.jpg", colorScheme: .light) + settingsModel.customBackground = .userImage(userImage) + XCTAssertEqual(provider.background, .userImage(.init(userImage))) + + settingsModel.customBackground = nil + XCTAssertEqual(provider.background, .default) + } + + func testThatBackgroundSetterSetsCorrectBackgroundInSettingsModel() throws { + provider.background = .gradient("gradient02.01") + XCTAssertEqual(settingsModel.customBackground, .gradient(.gradient0201)) + + provider.background = .solidColor("color02") + XCTAssertEqual(settingsModel.customBackground, .solidColor(.color02)) + + provider.background = .hexColor("#ABCDEF") + let hexColor = try XCTUnwrap(SolidColorBackground("#abcdef")) + XCTAssertEqual(settingsModel.customBackground, .solidColor(hexColor)) + + let userImage = UserBackgroundImage(fileName: "abc.jpg", colorScheme: .light) + provider.background = .userImage(.init(userImage)) + XCTAssertEqual(settingsModel.customBackground, .userImage(userImage)) + + provider.background = .default + XCTAssertEqual(settingsModel.customBackground, nil) + } + + @MainActor + func testThatCustomizerDataReturnsCorrectDataFromSettingsModelAndApperancePreferences() async throws { + try await waitForAvailableUserBackgroundImages { + userBackgroundImagesManager.availableImages = [ + .init(fileName: "1.jpg", colorScheme: .light), + .init(fileName: "2.jpg", colorScheme: .dark) + ] + } + + // this sets lastPickedCustomColor + settingsModel.customBackground = .solidColor(try XCTUnwrap(.init("#123abc"))) + settingsModel.customBackground = .solidColor(.color05) + appearancePreferences.currentThemeName = .light + + XCTAssertEqual( + provider.customizerData, + .init( + background: .solidColor("color05"), + theme: .light, + userColor: .init(hex: "#123abc"), + userImages: userBackgroundImagesManager.availableImages.map(NewTabPageDataModel.UserImage.init) + ) + ) + } + + // MARK: - Helpers + + /** + * Sets up an expectation, then sets up Combine subscription for `settingsModel.$availableUserBackgroundImages` that fulfills + * the expectation, then calls the provided `block` and waits for time specified by `duration` before cancelling the subscription. + */ + private func waitForAvailableUserBackgroundImages(for duration: TimeInterval = 1, _ block: () async -> Void = {}) async throws { + let expectation = self.expectation(description: "viewModelUpdate") + let cancellable = settingsModel.$availableUserBackgroundImages.dropFirst().prefix(1).sink { _ in expectation.fulfill() } + + await block() + + await fulfillment(of: [expectation], timeout: duration) + cancellable.cancel() + } +} From 1cdee8021ae956c1bac31f4790e01cf17e128fb8 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 12:17:30 +0100 Subject: [PATCH 56/62] Add remaining tests for NewTabPageCustomizationProvider --- .../NewTabPageCustomizationProvider.swift | 12 +- ...NewTabPageCustomizationProviderTests.swift | 128 +++++++++++++++++- 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift b/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift index d12faf0816..336d51c8ab 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageCustomizationProvider.swift @@ -79,7 +79,7 @@ final class NewTabPageCustomizationProvider: NewTabPageCustomBackgroundProviding } @MainActor - func presentUploadDialog() async{ + func presentUploadDialog() async { await homePageSettingsModel.addNewImage() } @@ -189,13 +189,3 @@ extension NewTabPageDataModel.Theme { } } } - -extension URL { - static func duckUserBackgroundImage(for fileName: String) -> URL? { - return URL(string: "duck://user-background-image/\(fileName)") - } - - static func duckUserBackgroundImageThumbnail(for fileName: String) -> URL? { - return URL(string: "duck://user-background-image/thumbnails/\(fileName)") - } -} diff --git a/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift b/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift index 1b895c015f..0a5d095b4b 100644 --- a/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift +++ b/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift @@ -17,6 +17,7 @@ // import AppKitExtensions +import Combine import NewTabPage import XCTest @testable import DuckDuckGo_Privacy_Browser @@ -26,6 +27,7 @@ final class NewTabPageCustomizationProviderTests: XCTestCase { var appearancePreferences: AppearancePreferences! var userColorProvider: MockUserColorProvider! var userBackgroundImagesManager: CapturingUserBackgroundImagesManager! + var openFilePanelCalls: Int = 0 private var settingsModel: HomePage.Models.SettingsModel! private var provider: NewTabPageCustomizationProvider! @@ -36,12 +38,16 @@ final class NewTabPageCustomizationProviderTests: XCTestCase { storageLocation = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) userBackgroundImagesManager = CapturingUserBackgroundImagesManager(storageLocation: storageLocation, maximumNumberOfImages: 4) userColorProvider = MockUserColorProvider() + openFilePanelCalls = 0 settingsModel = HomePage.Models.SettingsModel( appearancePreferences: appearancePreferences, userBackgroundImagesManager: userBackgroundImagesManager, sendPixel: { _ in }, - openFilePanel: { nil }, + openFilePanel: { + self.openFilePanelCalls += 1 + return nil + }, userColorProvider: self.userColorProvider, showAddImageFailedAlert: {}, navigator: MockHomePageSettingsModelNavigator() @@ -121,14 +127,132 @@ final class NewTabPageCustomizationProviderTests: XCTestCase { ) } + func testThatBackgroundPublisherPublishesEvents() throws { + var events: [NewTabPageDataModel.Background] = [] + let cancellable = provider.backgroundPublisher.sink { events.append($0) } + + settingsModel.customBackground = .gradient(.gradient04) + settingsModel.customBackground = .solidColor(.color13) + settingsModel.customBackground = .solidColor(.color13) + settingsModel.customBackground = .solidColor(.color13) + settingsModel.customBackground = .solidColor(.color13) + settingsModel.customBackground = .solidColor(try XCTUnwrap(.init("#123abc"))) + settingsModel.customBackground = nil + settingsModel.customBackground = .userImage(.init(fileName: "1.jpg", colorScheme: .light)) + + cancellable.cancel() + + XCTAssertEqual( + events, + [ + .gradient("gradient04"), + .solidColor("color13"), + .hexColor("#123abc"), + .default, + .userImage(.init(.init(fileName: "1.jpg", colorScheme: .light))) + ] + ) + } + + func testThatThemeGetterReturnsAppearancePreferencesTheme() { + appearancePreferences.currentThemeName = .dark + XCTAssertEqual(provider.theme, .dark) + appearancePreferences.currentThemeName = .light + XCTAssertEqual(provider.theme, .light) + appearancePreferences.currentThemeName = .systemDefault + XCTAssertEqual(provider.theme, nil) + } + + func testThatThemeSetterSetsAppearancePreferencesTheme() { + provider.theme = .dark + XCTAssertEqual(appearancePreferences.currentThemeName, .dark) + provider.theme = .light + XCTAssertEqual(appearancePreferences.currentThemeName, .light) + provider.theme = nil + XCTAssertEqual(appearancePreferences.currentThemeName, .systemDefault) + } + + func testThatThemePublisherPublishesEvents() throws { + var events: [NewTabPageDataModel.Theme?] = [] + let cancellable = provider.themePublisher.sink { events.append($0) } + + appearancePreferences.currentThemeName = .light + appearancePreferences.currentThemeName = .dark + appearancePreferences.currentThemeName = .dark + appearancePreferences.currentThemeName = .dark + appearancePreferences.currentThemeName = .dark + appearancePreferences.currentThemeName = .systemDefault + appearancePreferences.currentThemeName = .systemDefault + appearancePreferences.currentThemeName = .light + + cancellable.cancel() + + XCTAssertEqual(events, [.light, .dark, nil, .light]) + } + + func testThatUserImagesPublisherPublishesEvents() async throws { + var events: [[NewTabPageDataModel.UserImage]] = [] + let cancellable = provider.userImagesPublisher.sink { events.append($0) } + + let image1 = UserBackgroundImage(fileName: "1.jpg", colorScheme: .light) + let image2 = UserBackgroundImage(fileName: "2.jpg", colorScheme: .dark) + + try await waitForAvailableUserBackgroundImages { + userBackgroundImagesManager.availableImages = [image1] + } + try await waitForAvailableUserBackgroundImages { + userBackgroundImagesManager.availableImages = [image1, image2] + } + try await waitForAvailableUserBackgroundImages(inverted: true) { + userBackgroundImagesManager.availableImages = [image1, image2] + } + try await waitForAvailableUserBackgroundImages(inverted: true) { + userBackgroundImagesManager.availableImages = [image1, image2] + } + try await waitForAvailableUserBackgroundImages { + userBackgroundImagesManager.availableImages = [] + } + try await waitForAvailableUserBackgroundImages { + userBackgroundImagesManager.availableImages = [image2, image1] + } + + cancellable.cancel() + + XCTAssertEqual(events, [ + [.init(image1)], + [.init(image1), .init(image2)], + [], + [.init(image2), .init(image1)] + ]) + } + + func testThatPresentUploadDialogCallsAddImage() async { + await provider.presentUploadDialog() + XCTAssertEqual(openFilePanelCalls, 1) + } + + func testThatDeleteImageCallsImagesManager() async throws { + try await waitForAvailableUserBackgroundImages { + userBackgroundImagesManager.availableImages = [.init(fileName: "1.jpg", colorScheme: .light)] + } + await provider.deleteImage(with: "1.jpg") + XCTAssertEqual(userBackgroundImagesManager.deleteImageCallCount, 1) + } + + func testThatDeleteImageReturnsEarlyIfImageIsNotPresent() async { + await provider.deleteImage(with: "aaaaaa.jpg") + XCTAssertEqual(userBackgroundImagesManager.deleteImageCallCount, 0) + } + // MARK: - Helpers /** * Sets up an expectation, then sets up Combine subscription for `settingsModel.$availableUserBackgroundImages` that fulfills * the expectation, then calls the provided `block` and waits for time specified by `duration` before cancelling the subscription. */ - private func waitForAvailableUserBackgroundImages(for duration: TimeInterval = 1, _ block: () async -> Void = {}) async throws { + private func waitForAvailableUserBackgroundImages(for duration: TimeInterval = 1, inverted: Bool = false, _ block: () async -> Void = {}) async throws { let expectation = self.expectation(description: "viewModelUpdate") + expectation.isInverted = inverted let cancellable = settingsModel.$availableUserBackgroundImages.dropFirst().prefix(1).sink { _ in expectation.fulfill() } await block() From 2d39b28ad4597b8beaf8d63a2000e5a7deddd53c Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 12:41:01 +0100 Subject: [PATCH 57/62] Stabilize testThatUserImagesPublisherPublishesEvents --- .../NewTabPage/NewTabPageCustomizationProviderTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift b/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift index 0a5d095b4b..2fa49beae9 100644 --- a/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift +++ b/UnitTests/NewTabPage/NewTabPageCustomizationProviderTests.swift @@ -218,6 +218,11 @@ final class NewTabPageCustomizationProviderTests: XCTestCase { cancellable.cancel() + /// Slower machines may capture the initial empty array event so let's filter it out here + if events.first == [] { + events = Array(events.dropFirst()) + } + XCTAssertEqual(events, [ [.init(image1)], [.init(image1), .init(image2)], From 0c72d99fea461244068fa3476148fa980bda4dc6 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 12:45:01 +0100 Subject: [PATCH 58/62] Add documentation for NewTabPageCustomizerOpener --- ...splayer.swift => NewTabPageCustomizerOpener.swift} | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) rename LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/{NewTabPageCustomizerDisplayer.swift => NewTabPageCustomizerOpener.swift} (70%) diff --git a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerOpener.swift similarity index 70% rename from LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift rename to LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerOpener.swift index 3e27cf0563..e6bf41214d 100644 --- a/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerDisplayer.swift +++ b/LocalPackages/NewTabPage/Sources/NewTabPage/CustomBackground/NewTabPageCustomizerOpener.swift @@ -1,5 +1,5 @@ // -// NewTabPageCustomizerDisplayer.swift +// NewTabPageCustomizerOpener.swift // // Copyright © 2024 DuckDuckGo. All rights reserved. // @@ -19,6 +19,15 @@ import Combine import WebKit +/** + * This small class exposes an interface that allows for triggering + * events that should open New Tab Page settings. + * + * It's a requirement in `NewTabPageCustomBackgroundProviding` protocol + * and must be provided by classes implementing that protocol on the client app side. + * `NewTabPageCustomBackgroundClient` connects to the opener and forwards + * open settings requests to the JS side. + */ public final class NewTabPageCustomizerOpener { public init() { openSettingsPublisher = openSettingsSubject.eraseToAnyPublisher() From c1924dd2612ce4379ab3434f4111246ba12c6a57 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 12:58:07 +0100 Subject: [PATCH 59/62] Revert changes to NewTabPageWebViewModel --- DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift index 5fb37935de..e8eb6fdef0 100644 --- a/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift +++ b/DuckDuckGo/NewTabPage/NewTabPageWebViewModel.swift @@ -37,11 +37,7 @@ final class NewTabPageWebViewModel: NSObject { let webView: WebView private var windowCancellable: AnyCancellable? - init( - featureFlagger: FeatureFlagger, - actionsManager: NewTabPageActionsManaging, - activeRemoteMessageModel: ActiveRemoteMessageModel - ) { + init(featureFlagger: FeatureFlagger, actionsManager: NewTabPageActionsManaging, activeRemoteMessageModel: ActiveRemoteMessageModel) { newTabPageUserScript = NewTabPageUserScript() actionsManager.registerUserScript(newTabPageUserScript) @@ -59,7 +55,6 @@ final class NewTabPageWebViewModel: NSObject { .map { $0 != nil } .sink { [weak activeRemoteMessageModel] isOnScreen in activeRemoteMessageModel?.isViewOnScreen = isOnScreen - if isOnScreen { NotificationCenter.default.post(name: .newTabPageWebViewDidAppear, object: nil) } From 90a06074d85bbbe6de8aed80fc5ce4c4d27dc7ff Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 13:51:02 +0100 Subject: [PATCH 60/62] Remove MainActor requirement from FreemiumDBPPromotionViewCoordinatorTests functions --- .../Freemium/DBP/FreemiumDBPPresenter.swift | 1 + ...reemiumDBPPromotionViewCoordinatorTests.swift | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/DuckDuckGo/Freemium/DBP/FreemiumDBPPresenter.swift b/DuckDuckGo/Freemium/DBP/FreemiumDBPPresenter.swift index 06654880bf..3b92893c47 100644 --- a/DuckDuckGo/Freemium/DBP/FreemiumDBPPresenter.swift +++ b/DuckDuckGo/Freemium/DBP/FreemiumDBPPresenter.swift @@ -21,6 +21,7 @@ import Freemium /// Conforming types provide functionality to show Freemium DBP protocol FreemiumDBPPresenter { + @MainActor func showFreemiumDBPAndSetActivated(windowControllerManager: WindowControllersManagerProtocol?) } diff --git a/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift b/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift index b565768f58..2764ed2857 100644 --- a/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift +++ b/UnitTests/Freemium/DBP/FreemiumDBPPromotionViewCoordinatorTests.swift @@ -33,7 +33,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { private var mockPixelHandler: MockFreemiumDBPExperimentPixelHandler! private var cancellables: Set = [] - @MainActor override func setUpWithError() throws { mockUserStateManager = MockFreemiumDBPUserStateManager() mockFeature = MockFreemiumDBPFeature() @@ -57,7 +56,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { mockPresenter = nil } - @MainActor func testInitialPromotionVisibility_whenFeatureIsAvailable_andNotDismissed() { // Given mockUserStateManager.didDismissHomePagePromotion = false @@ -74,7 +72,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertTrue(sut.isHomePagePromotionVisible) } - @MainActor func testInitialPromotionVisibility_whenPromotionDismissed() { // Given mockUserStateManager.didDismissHomePagePromotion = true @@ -91,7 +88,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertFalse(sut.isHomePagePromotionVisible) } - @MainActor func testProceedAction_dismissesPromotion_callsShowFreemium_andFiresPixel() async throws { // Given try await waitForViewModelUpdate { @@ -109,7 +105,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(mockPixelHandler.lastFiredEvent, FreemiumDBPExperimentPixel.newTabScanClick) } - @MainActor func testCloseAction_dismissesPromotion_andFiresPixel() async throws { // When try await waitForViewModelUpdate() @@ -121,7 +116,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(mockPixelHandler.lastFiredEvent, FreemiumDBPExperimentPixel.newTabScanDismiss) } - @MainActor func testProceedAction_dismissesResults_callsShowFreemium_andFiresPixel() async throws { // Given try await waitForViewModelUpdate { @@ -139,7 +133,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(mockPixelHandler.lastFiredEvent, FreemiumDBPExperimentPixel.newTabResultsClick) } - @MainActor func testCloseAction_dismissesResults_andFiresPixel() async throws { // Given try await waitForViewModelUpdate { @@ -155,7 +148,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(mockPixelHandler.lastFiredEvent, FreemiumDBPExperimentPixel.newTabResultsDismiss) } - @MainActor func testProceedAction_dismissesNoResults_callsShowFreemium_andFiresPixel() async throws { // Given try await waitForViewModelUpdate { @@ -173,7 +165,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(mockPixelHandler.lastFiredEvent, FreemiumDBPExperimentPixel.newTabNoResultsClick) } - @MainActor func testCloseAction_dismissesNoResults_andFiresPixel() async throws { // Given try await waitForViewModelUpdate { @@ -189,7 +180,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(mockPixelHandler.lastFiredEvent, FreemiumDBPExperimentPixel.newTabNoResultsDismiss) } - @MainActor func testViewModel_whenResultsExist_withMatches() async throws { // Given try await waitForViewModelUpdate { @@ -203,7 +193,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(viewModel?.description, UserText.homePagePromotionFreemiumDBPPostScanEngagementResultPluralDescription(resultCount: 5, brokerCount: 2)) } - @MainActor func testViewModel_whenNoResultsExist() async throws { // Given let viewModel = try await waitForViewModelUpdate { @@ -214,7 +203,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertEqual(viewModel?.description, UserText.homePagePromotionFreemiumDBPDescriptionMarkdown) } - @MainActor func testViewModel_whenFeatureNotEnabled() async throws { // Given try await waitForViewModelUpdate { @@ -242,7 +230,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertFalse(mockUserStateManager.didDismissHomePagePromotion) } - @MainActor func testHomePageBecomesVisible_whenFeatureBecomesAvailable_andDidDismissFalse() { // Given mockUserStateManager.didDismissHomePagePromotion = false @@ -272,7 +259,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertTrue(sut.isHomePagePromotionVisible) } - @MainActor func testHomePageBecomesInVisible_whenFeatureBecomesUnAvailable_andDidDismissFalse() { // Given mockUserStateManager.didDismissHomePagePromotion = false @@ -302,7 +288,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertFalse(sut.isHomePagePromotionVisible) } - @MainActor func testHomePageDoesNotBecomeVisible_whenFeatureBecomesAvailable_andDidDismissTrue() { // Given mockUserStateManager.didDismissHomePagePromotion = true @@ -332,7 +317,6 @@ final class FreemiumDBPPromotionViewCoordinatorTests: XCTestCase { XCTAssertFalse(sut.isHomePagePromotionVisible) } - @MainActor func testHomePageDoesNotBecomeVisible_whenFeatureBecomesUnAvailable_andDidDismissTrue() { // Given mockUserStateManager.didDismissHomePagePromotion = true From 5d173a351760542370ed78ca8878853ca22ba4c4 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Wed, 8 Jan 2025 14:06:28 +0100 Subject: [PATCH 61/62] Always call proceedAction on Main Actor --- .../Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift b/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift index d49414b0b1..365a66f075 100644 --- a/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift +++ b/DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift @@ -94,7 +94,7 @@ private extension FreemiumDBPPromotionViewCoordinator { /// Action to be executed when the user proceeds with the promotion (e.g opens DBP) var proceedAction: () async -> Void { - { [weak self] in + { @MainActor [weak self] in guard let self else { return } execute(resultsAction: { @@ -105,7 +105,7 @@ private extension FreemiumDBPPromotionViewCoordinator { self.freemiumDBPExperimentPixelHandler.fire(FreemiumDBPExperimentPixel.newTabScanClick) }) - await showFreemiumDBP() + showFreemiumDBP() dismissHomePagePromotion() } } From 60900854254654dc537194d5357dcfa8ee096e77 Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Thu, 9 Jan 2025 09:49:49 +0100 Subject: [PATCH 62/62] Display native NTP in Fire Window --- DuckDuckGo/Tab/View/BrowserTabViewController.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Tab/View/BrowserTabViewController.swift index 88d3928293..2305135c74 100644 --- a/DuckDuckGo/Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Tab/View/BrowserTabViewController.swift @@ -811,7 +811,8 @@ final class BrowserTabViewController: NSViewController { updateTabIfNeeded(tabViewModel: tabViewModel) case .newtab: - if featureFlagger.isFeatureOn(.htmlNewTabPage) { + // We only use HTML New Tab Page in regular windows for now + if featureFlagger.isFeatureOn(.htmlNewTabPage) && !tabCollectionViewModel.isBurner { updateTabIfNeeded(tabViewModel: tabViewModel) } else { removeAllTabContent()