From 91738a9d2876fb64409c90a480ea0d2530b5338e Mon Sep 17 00:00:00 2001 From: Dominik Kapusta Date: Fri, 29 Nov 2024 23:33:11 +0100 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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) {