diff --git a/.maestro/data_clearing_tests/02_duckduckgo_settings.yml b/.maestro/data_clearing_tests/02_duckduckgo_settings.yml index e2da0f3e3a..fa68b0c05a 100644 --- a/.maestro/data_clearing_tests/02_duckduckgo_settings.yml +++ b/.maestro/data_clearing_tests/02_duckduckgo_settings.yml @@ -40,6 +40,11 @@ tags: id: "alert.forget-data.confirm" - assertVisible: id: "searchEntry" +- runFlow: + when: + visible: "Cancel" + commands: + - tapOn: "Cancel" - tapOn: "Close Tabs and Clear Data" - tapOn: id: "alert.forget-data.confirm" diff --git a/.swiftlint.yml b/.swiftlint.yml index 5808536458..9047bcb725 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -7,6 +7,7 @@ disabled_rules: - todo - unused_capture_list - trailing_comma + - cyclomatic_complexity opt_in_rules: - closure_end_indentation diff --git a/Core/BookmarksCachingSearch.swift b/Core/BookmarksCachingSearch.swift index af6ea20826..a874a05284 100644 --- a/Core/BookmarksCachingSearch.swift +++ b/Core/BookmarksCachingSearch.swift @@ -60,19 +60,7 @@ public class CoreDataBookmarksSearchStore: BookmarksSearchStore { public func bookmarksAndFavorites(completion: @escaping ([BookmarksCachingSearch.ScoredBookmark]) -> Void) { let context = bookmarksStore.makeContext(concurrencyType: .privateQueueConcurrencyType) - - let fetchRequest = NSFetchRequest(entityName: "BookmarkEntity") - fetchRequest.predicate = NSPredicate( - format: "%K = false AND %K == NO AND (%K == NO OR %K == nil)", - #keyPath(BookmarkEntity.isFolder), - #keyPath(BookmarkEntity.isPendingDeletion), - #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub) - ) - fetchRequest.resultType = .dictionaryResultType - fetchRequest.propertiesToFetch = [#keyPath(BookmarkEntity.title), - #keyPath(BookmarkEntity.url), - #keyPath(BookmarkEntity.objectID)] - fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(BookmarkEntity.favoriteFolders)] + let fetchRequest = Self.shallowBookmarksFetchRequest(context: context) context.perform { let result = try? context.fetch(fetchRequest) as? [[String: Any]] @@ -97,6 +85,38 @@ public class CoreDataBookmarksSearchStore: BookmarksSearchStore { externalContext.persistentStoreCoordinator == bookmarksStore.coordinator else { return } subject.send() } + + /// Creates an `NSFetchRequest` to retrieve each bookmark as a shallow dictionary. + /// + /// The dictionary contains + /// * `#keyPath(BookmarkEntity.title)` + /// * `#keyPath(BookmarkEntity.url)` + /// * `#keyPath(BookmarkEntity.objectID)` + /// * `#keyPath(BookmarkEntity.favoriteFolders)` + /// + /// Note that is `#keyPath(BookmarkEntity.favoriteFolders)` an `Int` representing the count of favorites folders this bookmark is contained in + public static func shallowBookmarksFetchRequest(context: NSManagedObjectContext) -> NSFetchRequest { + let favExpression = NSExpressionDescription() + favExpression.name = #keyPath(BookmarkEntity.favoriteFolders) + favExpression.expression = NSExpression(forFunction: "count:", + arguments: [NSExpression(forKeyPath: #keyPath(BookmarkEntity.favoriteFolders))]) + favExpression.expressionResultType = .integer64AttributeType + + let fetchRequest = NSFetchRequest(entityName: "BookmarkEntity") + fetchRequest.predicate = NSPredicate( + format: "%K = false AND %K == NO AND (%K == NO OR %K == nil)", + #keyPath(BookmarkEntity.isFolder), + #keyPath(BookmarkEntity.isPendingDeletion), + #keyPath(BookmarkEntity.isStub), #keyPath(BookmarkEntity.isStub) + ) + fetchRequest.resultType = .dictionaryResultType + fetchRequest.propertiesToFetch = [#keyPath(BookmarkEntity.title), + #keyPath(BookmarkEntity.url), + #keyPath(BookmarkEntity.objectID), + favExpression] + + return fetchRequest + } } public class BookmarksCachingSearch: BookmarksStringSearch { @@ -125,14 +145,16 @@ public class BookmarksCachingSearch: BookmarksStringSearch { guard let title = bookmark[#keyPath(BookmarkEntity.title)] as? String, let urlString = bookmark[#keyPath(BookmarkEntity.url)] as? String, let url = URL(string: urlString), - let objectID = bookmark[#keyPath(BookmarkEntity.objectID)] as? NSManagedObjectID else { + let objectID = bookmark[#keyPath(BookmarkEntity.objectID)] as? NSManagedObjectID, + let favoritesFolderCount = bookmark[#keyPath(BookmarkEntity.favoriteFolders)] as? Int + else { return nil } self.init(objectID: objectID, title: title, url: url, - isFavorite: (bookmark[#keyPath(BookmarkEntity.favoriteFolders)] as? Set)?.isEmpty != true) + isFavorite: favoritesFolderCount > 0) } public func togglingFavorite() -> BookmarksStringSearchResult { diff --git a/Core/Debounce.swift b/Core/Debounce.swift deleted file mode 100644 index 6994c3dda3..0000000000 --- a/Core/Debounce.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// Debounce.swift -// DuckDuckGo -// -// Copyright © 2019 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 class Debounce { - - private let queue: DispatchQueue - private let interval: TimeInterval - - private var currentWorkItem = DispatchWorkItem(block: {}) - - public init(queue: DispatchQueue, seconds: TimeInterval) { - self.queue = queue - self.interval = seconds - } - - public func schedule(_ block: @escaping (() -> Void)) { - currentWorkItem.cancel() - currentWorkItem = DispatchWorkItem(block: { block() }) - queue.asyncAfter(deadline: .now() + interval, execute: currentWorkItem) - } -} diff --git a/Core/DefaultVariantManager.swift b/Core/DefaultVariantManager.swift index b17b8de45d..98cbf59382 100644 --- a/Core/DefaultVariantManager.swift +++ b/Core/DefaultVariantManager.swift @@ -62,6 +62,10 @@ public struct VariantIOS: Variant { VariantIOS(name: "sc", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "sd", weight: doNotAllocate, isIncluded: When.always, features: []), VariantIOS(name: "se", weight: doNotAllocate, isIncluded: When.always, features: []), + + VariantIOS(name: "mc", weight: 1, isIncluded: When.inEnglish, features: []), + VariantIOS(name: "md", weight: 1, isIncluded: When.inEnglish, features: [.history]), + returningUser ] diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 1246732321..f14ae54cbf 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -34,7 +34,6 @@ public enum FeatureFlag: String { case networkProtection case networkProtectionWaitlistAccess case networkProtectionWaitlistActive - case subscription case swipeTabs case autoconsentOnByDefault case history @@ -43,7 +42,7 @@ public enum FeatureFlag: String { extension FeatureFlag: FeatureFlagSourceProviding { public var source: FeatureFlagSource { switch self { - case .debugMenu, .appTrackingProtection, .subscription, .swipeTabs: + case .debugMenu, .appTrackingProtection, .swipeTabs: return .internalOnly case .sync: return .remoteReleasable(.subfeature(SyncSubfeature.level0ShowSync)) @@ -71,6 +70,7 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .remoteReleasable(.subfeature(AutoconsentSubfeature.onByDefault)) case .history: return .remoteReleasable(.feature(.history)) + } } } diff --git a/Core/HistoryCapture.swift b/Core/HistoryCapture.swift index d63c7fd917..68e90a8953 100644 --- a/Core/HistoryCapture.swift +++ b/Core/HistoryCapture.swift @@ -40,17 +40,16 @@ public class HistoryCapture { public func webViewDidCommit(url: URL) { self.url = url - coordinator.addVisit(of: url.urlOrDuckDuckGoCleanQuery) + coordinator.addVisit(of: url) } public func titleDidChange(_ title: String?, forURL url: URL?) { - guard self.url == url else { + guard let url, self.url == url else { return } - guard let url = url?.urlOrDuckDuckGoCleanQuery, let title, !title.isEmpty else { - return - } + guard let title, !title.isEmpty else { return } + coordinator.updateTitleIfNeeded(title: title, url: url) coordinator.commitChanges(url: url) } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 472635c170..a4d1571bdd 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -100,9 +100,14 @@ extension Pixel { case homeScreenEditFavorite case homeScreenDeleteFavorite - case autocompleteSelectedLocal - case autocompleteSelectedRemote - + case autocompleteEnabled + case autocompleteDisabled + case autocompleteClickPhrase + case autocompleteClickWebsite + case autocompleteClickBookmark + case autocompleteClickFavorite + case autocompleteClickHistory + case feedbackPositive case feedbackNegativePrefix(category: String) @@ -596,6 +601,11 @@ extension Pixel { case privacyProSubscriptionManagementEmail case privacyProSubscriptionManagementPlanBilling case privacyProSubscriptionManagementRemoval + // Web pixels + case privacyProOfferMonthlyPriceClick + case privacyProOfferYearlyPriceClick + case privacyProAddEmailSuccess + case privacyProWelcomeFAQClick } } @@ -677,9 +687,14 @@ extension Pixel.Event { case .homeScreenEditFavorite: return "mh_ef" case .homeScreenDeleteFavorite: return "mh_df" - case .autocompleteSelectedLocal: return "m_au_l" - case .autocompleteSelectedRemote: return "m_au_r" - + case .autocompleteEnabled: return "m_autocomplete_toggled_on" + case .autocompleteDisabled: return "m_autocomplete_toggled_off" + case .autocompleteClickPhrase: return "m_autocomplete_click_phrase" + case .autocompleteClickWebsite: return "m_autocomplete_click_website" + case .autocompleteClickBookmark: return "m_autocomplete_click_bookmark" + case .autocompleteClickFavorite: return "m_autocomplete_click_favorite" + case .autocompleteClickHistory: return "m_autocomplete_click_history" + case .feedbackPositive: return "mfbs_positive_submit" case .feedbackNegativePrefix(category: let category): return "mfbs_negative_\(category)" @@ -1133,7 +1148,7 @@ extension Pixel.Event { case .historyInsertVisitFailed: return "m_debug_history-insert-visit-failed" case .historyRemoveVisitsFailed: return "m_debug_history-remove-visits-failed" - // MARK: Privacy pro + // MARK: Privacy pro case .privacyProSubscriptionActive: return "m_privacy-pro_app_subscription_active" case .privacyProOfferScreenImpression: return "m_privacy-pro_offer_screen_impression" case .privacyProPurchaseAttempt: return "m_privacy-pro_terms-conditions_subscribe_click" @@ -1164,6 +1179,11 @@ extension Pixel.Event { case .privacyProSubscriptionManagementEmail: return "m_privacy-pro_manage-email_edit_click" case .privacyProSubscriptionManagementPlanBilling: return "m_privacy-pro_settings_change-plan-or-billing_click" case .privacyProSubscriptionManagementRemoval: return "m_privacy-pro_settings_remove-from-device_click" + // Web + case .privacyProOfferMonthlyPriceClick: return "m_privacy-pro_offer_monthly-price_click" + case .privacyProOfferYearlyPriceClick: return "m_privacy-pro_offer_yearly-price_click" + case .privacyProAddEmailSuccess: return "m_privacy-pro_app_add-email_success_u" + case .privacyProWelcomeFAQClick: return "m_privacy-pro_welcome_faq_click_u" } } } diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 57155c48ce..1fb2080c94 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -296,6 +296,8 @@ 4BBBBA902B031B4200D965DA /* VPNWaitlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA8C2B031B4200D965DA /* VPNWaitlist.swift */; }; 4BBBBA922B03291700D965DA /* VPNWaitlistUserText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */; }; 4BC21A2F27238B7500229F0E /* RunLoopExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */; }; + 4BCBE45E2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */; }; + 4BCBE4602BA7E87100FC75A1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */; }; 4BCD14632B05AF2B000B1E4C /* NetworkProtectionAccessController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */; }; 4BCD14672B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */; }; 4BCD14692B05BDD5000B1E4C /* AppDelegate+Waitlists.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BCD14682B05BDD5000B1E4C /* AppDelegate+Waitlists.swift */; }; @@ -376,6 +378,7 @@ 851CD674244D7E6000331B98 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85449F0023FEAF3000512AAF /* UserDefaultsExtension.swift */; }; 851DFD87212C39D300D95F20 /* TabSwitcherButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851DFD86212C39D300D95F20 /* TabSwitcherButton.swift */; }; 851DFD8A212C5EE800D95F20 /* TabSwitcherButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 851DFD89212C5EE800D95F20 /* TabSwitcherButtonTests.swift */; }; + 851F74262B9A1BFD00747C42 /* Suggestions in Frameworks */ = {isa = PBXBuildFile; productRef = 851F74252B9A1BFD00747C42 /* Suggestions */; }; 85200FA11FBC5BB5001AF290 /* DDGPersistenceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85200FA01FBC5BB5001AF290 /* DDGPersistenceContainer.swift */; }; 8521FDE6238D414B00A44CC3 /* FileStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8521FDE4238D411400A44CC3 /* FileStoreTests.swift */; }; 8524AAAC2A3888FE00EEC6D2 /* Waitlist.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8524AAAB2A3888FE00EEC6D2 /* Waitlist.xcassets */; }; @@ -427,6 +430,7 @@ 8551912724746EDC0010FDD0 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8551912624746EDC0010FDD0 /* SnapshotHelper.swift */; }; 85582E0029D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85582DFF29D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift */; }; 855D914D2063EF6A00C4B448 /* TabSwitcherTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 855D914C2063EF6A00C4B448 /* TabSwitcherTransition.swift */; }; + 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8562CE142B9B645C00E1D399 /* CachedBookmarkSuggestions.swift */; }; 8563A03C1F9288D600F04442 /* BrowserChromeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8563A03B1F9288D600F04442 /* BrowserChromeManager.swift */; }; 8565A34B1FC8D96B00239327 /* LaunchTabNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */; }; 8565A34D1FC8DFE400239327 /* LaunchTabNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */; }; @@ -503,7 +507,6 @@ 85DFEDF724CB1CAB00973FE7 /* ShareSheet.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85DFEDF624CB1CAB00973FE7 /* ShareSheet.xcassets */; }; 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85DFEDF824CF3D0E00973FE7 /* TabsBarCell.swift */; }; 85E242172AB1B54D000F3E28 /* ReturnUserMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E242162AB1B54D000F3E28 /* ReturnUserMeasurement.swift */; }; - 85E5603026541D9E00F4DC44 /* AutocompleteRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E5602E26541D1D00F4DC44 /* AutocompleteRequestTests.swift */; }; 85E58C2C28FDA94F006A801A /* FavoritesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85E58C2B28FDA94F006A801A /* FavoritesViewController.swift */; }; 85EE7F55224667DD000FE757 /* WebContainer.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85EE7F54224667DD000FE757 /* WebContainer.storyboard */; }; 85EE7F572246685B000FE757 /* WebContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EE7F562246685B000FE757 /* WebContainerViewController.swift */; }; @@ -592,7 +595,6 @@ 988AC355257E47C100793C64 /* RequeryLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988AC354257E47C100793C64 /* RequeryLogic.swift */; }; 988F3DCF237D5C0F00AEE34C /* SchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988F3DCE237D5C0F00AEE34C /* SchemeHandler.swift */; }; 988F3DD3237DE8D900AEE34C /* ForgetDataAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988F3DD2237DE8D900AEE34C /* ForgetDataAlert.swift */; }; - 98982B3422F8D8E400578AC9 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98982B3322F8D8E400578AC9 /* Debounce.swift */; }; 98983096255B5019003339A2 /* BookmarksCachingSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98983095255B5019003339A2 /* BookmarksCachingSearchTests.swift */; }; 98999D5922FDA41500CBBE1B /* BasicAuthenticationAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98999D5822FDA41500CBBE1B /* BasicAuthenticationAlert.swift */; }; 989B337522D7EF2100437824 /* EmptyCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 989B337422D7EF2100437824 /* EmptyCollectionReusableView.swift */; }; @@ -711,6 +713,11 @@ BDA583892B98BA7600732FDC /* AccountManagerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA583862B98B6C700732FDC /* AccountManagerExtension.swift */; }; BDC234F72B27F51100D3C798 /* UniquePixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC234F62B27F51100D3C798 /* UniquePixel.swift */; }; BDD3B3552B8EF8DB005857A8 /* NetworkProtectionUNNotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE3766DD2AC5945500AAB575 /* NetworkProtectionUNNotificationPresenter.swift */; }; + BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */; }; + BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */; }; + BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */; }; + BDFF03232BA3D8E300F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */; }; + BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */; }; C10CB5F32A1A5BDF0048E503 /* AutofillViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */; }; C111B26927F579EF006558B1 /* BookmarkOrFolderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */; }; C12726EE2A5FF88C00215B02 /* EmailSignupPromptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */; }; @@ -930,7 +937,6 @@ F176699F1E40BC86003D3222 /* Settings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F176699D1E40BC86003D3222 /* Settings.storyboard */; }; F17669D71E43401C003D3222 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17669D61E43401C003D3222 /* MainViewController.swift */; }; F17843E91F36226700390DCD /* MockFiles in Resources */ = {isa = PBXBuildFile; fileRef = F17843E81F36226700390DCD /* MockFiles */; }; - F17922DB1E717C8D006E3D97 /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922DA1E717C8D006E3D97 /* Suggestion.swift */; }; F17922DE1E7192E6006E3D97 /* SuggestionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922DD1E7192E6006E3D97 /* SuggestionTableViewCell.swift */; }; F17922E01E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */; }; F17922E21E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F17922E11E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift */; }; @@ -942,7 +948,6 @@ F194FAFB1F14E622009B4DF8 /* UIFontExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F194FAFA1F14E622009B4DF8 /* UIFontExtensionTests.swift */; }; F198D78E1E39762C0088DA8A /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F198D78D1E39762C0088DA8A /* StringExtensionTests.swift */; }; F198D7981E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */; }; - F1A5683A1E70F98E0081082E /* AutocompleteRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A568391E70F98E0081082E /* AutocompleteRequest.swift */; }; F1A886781F29394E0096251E /* WebCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A886771F29394E0096251E /* WebCacheManager.swift */; }; F1AE54E81F0425FC00D9A700 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1AE54E71F0425FC00D9A700 /* AuthenticationViewController.swift */; }; F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1BE54571E69DE1000FCF649 /* TutorialSettings.swift */; }; @@ -1412,6 +1417,8 @@ 4BBBBA8C2B031B4200D965DA /* VPNWaitlist.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VPNWaitlist.swift; sourceTree = ""; }; 4BBBBA912B03291700D965DA /* VPNWaitlistUserText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNWaitlistUserText.swift; sourceTree = ""; }; 4BC21A2C272388BD00229F0E /* RunLoopExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopExtensionTests.swift; sourceTree = ""; }; + 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 4BCD14622B05AF2B000B1E4C /* NetworkProtectionAccessController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionAccessController.swift; sourceTree = ""; }; 4BCD14662B05B682000B1E4C /* NetworkProtectionTermsAndConditionsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionTermsAndConditionsStore.swift; sourceTree = ""; }; 4BCD14682B05BDD5000B1E4C /* AppDelegate+Waitlists.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+Waitlists.swift"; sourceTree = ""; }; @@ -1542,6 +1549,7 @@ 8551912624746EDC0010FDD0 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; }; 85582DFF29D7409700E9AE35 /* SyncSettingsViewController+PDFRendering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SyncSettingsViewController+PDFRendering.swift"; sourceTree = ""; }; 855D914C2063EF6A00C4B448 /* TabSwitcherTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSwitcherTransition.swift; sourceTree = ""; }; + 8562CE142B9B645C00E1D399 /* CachedBookmarkSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedBookmarkSuggestions.swift; sourceTree = ""; }; 8563A03B1F9288D600F04442 /* BrowserChromeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserChromeManager.swift; sourceTree = ""; }; 8565A34A1FC8D96B00239327 /* LaunchTabNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTabNotification.swift; sourceTree = ""; }; 8565A34C1FC8DFE400239327 /* LaunchTabNotificationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchTabNotificationTests.swift; sourceTree = ""; }; @@ -1616,7 +1624,6 @@ 85DFEDF624CB1CAB00973FE7 /* ShareSheet.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = ShareSheet.xcassets; sourceTree = ""; }; 85DFEDF824CF3D0E00973FE7 /* TabsBarCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsBarCell.swift; sourceTree = ""; }; 85E242162AB1B54D000F3E28 /* ReturnUserMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnUserMeasurement.swift; sourceTree = ""; }; - 85E5602E26541D1D00F4DC44 /* AutocompleteRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocompleteRequestTests.swift; sourceTree = ""; }; 85E58C2B28FDA94F006A801A /* FavoritesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesViewController.swift; sourceTree = ""; }; 85EE7F54224667DD000FE757 /* WebContainer.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = WebContainer.storyboard; sourceTree = ""; }; 85EE7F562246685B000FE757 /* WebContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebContainerViewController.swift; sourceTree = ""; }; @@ -2205,7 +2212,6 @@ 988F3DCE237D5C0F00AEE34C /* SchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchemeHandler.swift; sourceTree = ""; }; 988F3DD2237DE8D900AEE34C /* ForgetDataAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForgetDataAlert.swift; sourceTree = ""; }; 9896632322C56716007BE4FE /* EtagStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EtagStorage.swift; sourceTree = ""; }; - 98982B3322F8D8E400578AC9 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; 98983095255B5019003339A2 /* BookmarksCachingSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksCachingSearchTests.swift; sourceTree = ""; }; 98987E6E251EAC3B006F75CD /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/InfoPlist.strings; sourceTree = ""; }; 98987E70251EAC3B006F75CD /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -2366,6 +2372,10 @@ BD862E0A2B30F9300073E2EE /* VPNFeedbackFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormView.swift; sourceTree = ""; }; BDA583862B98B6C700732FDC /* AccountManagerExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountManagerExtension.swift; sourceTree = ""; }; BDC234F62B27F51100D3C798 /* UniquePixel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniquePixel.swift; sourceTree = ""; }; + BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibility.swift; sourceTree = ""; }; + BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNetworkProtectionVisibility.swift; sourceTree = ""; }; + BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionVisibilityForTunnelProvider.swift; sourceTree = ""; }; + BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionFeatureVisibilityTests.swift; sourceTree = ""; }; C10CB5F22A1A5BDF0048E503 /* AutofillViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillViews.swift; sourceTree = ""; }; C111B26827F579EF006558B1 /* BookmarkOrFolderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkOrFolderTests.swift; sourceTree = ""; }; C12726ED2A5FF88C00215B02 /* EmailSignupPromptView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailSignupPromptView.swift; sourceTree = ""; }; @@ -2628,7 +2638,6 @@ F176699E1E40BC86003D3222 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Settings.storyboard; sourceTree = ""; }; F17669D61E43401C003D3222 /* MainViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; F17843E81F36226700390DCD /* MockFiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MockFiles; sourceTree = ""; }; - F17922DA1E717C8D006E3D97 /* Suggestion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = ""; }; F17922DD1E7192E6006E3D97 /* SuggestionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuggestionTableViewCell.swift; sourceTree = ""; }; F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteViewControllerDelegate.swift; sourceTree = ""; }; F17922E11E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoSuggestionsTableViewCell.swift; sourceTree = ""; }; @@ -2641,7 +2650,6 @@ F197EA3B1E6885F20029BDC1 /* TextFieldWithInsets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextFieldWithInsets.swift; path = ../Core/TextFieldWithInsets.swift; sourceTree = ""; }; F198D78D1E39762C0088DA8A /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = ""; }; F198D7971E3A45D90088DA8A /* WKWebViewConfigurationExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewConfigurationExtensionTests.swift; sourceTree = ""; }; - F1A568391E70F98E0081082E /* AutocompleteRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutocompleteRequest.swift; sourceTree = ""; }; F1A886771F29394E0096251E /* WebCacheManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebCacheManager.swift; sourceTree = ""; }; F1AA54601E48D90700223211 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; F1AE54E71F0425FC00D9A700 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; @@ -2817,6 +2825,7 @@ EE8E568A2A56BCE400F11DCA /* NetworkProtection in Frameworks */, CBC83E3429B631780008E19C /* Configuration in Frameworks */, D61CDA182B7CF78300A0FBB9 /* ZIPFoundation in Frameworks */, + 851F74262B9A1BFD00747C42 /* Suggestions in Frameworks */, 98A16C2D28A11D6200A6C003 /* BrowserServicesKit in Frameworks */, 8599690F29D2F1C100DBF9FA /* DDGSync in Frameworks */, 851481882A600EFC00ABC65F /* RemoteMessaging in Frameworks */, @@ -2854,6 +2863,7 @@ 02025669298818B200E694E7 /* PacketTunnelProvider.entitlements */, EEFC6A5F2AC0F2F80065027D /* UserText.swift */, EEDFE2DC2AC6ED4F00F0E19C /* Localizable.strings */, + 4BCBE45F2BA7E87100FC75A1 /* PrivacyInfo.xcprivacy */, ); path = PacketTunnelProvider; sourceTree = ""; @@ -3587,6 +3597,7 @@ 4BCD146A2B05C4B5000B1E4C /* VPNWaitlistTermsAndConditionsViewController.swift */, 4B78074B2B1823C5009DB2CF /* VPNWaitlistActivationDateStore.swift */, 4B78074D2B183A1F009DB2CF /* SurveyURLBuilder.swift */, + BDFF031F2BA3D3AD00F324C9 /* Feature Visibility */, ); name = VPN; sourceTree = ""; @@ -3855,6 +3866,7 @@ isa = PBXGroup; children = ( 8512EA5624ED30D30073EE19 /* Assets.xcassets */, + 4BCBE45D2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy */, 853273AC24FEF49600E3C778 /* ColorExtension.swift */, 853273B124FF114700E3C778 /* DeepLinks.swift */, 8512EA5824ED30D30073EE19 /* Info.plist */, @@ -4133,14 +4145,6 @@ name = iPad; sourceTree = ""; }; - 85E5602D26541D0900F4DC44 /* AutoComplete */ = { - isa = PBXGroup; - children = ( - 85E5602E26541D1D00F4DC44 /* AutocompleteRequestTests.swift */, - ); - name = AutoComplete; - sourceTree = ""; - }; 85EE7F53224667C3000FE757 /* WebContainer */ = { isa = PBXGroup; children = ( @@ -4436,6 +4440,16 @@ name = Subscription; sourceTree = ""; }; + BDFF031F2BA3D3AD00F324C9 /* Feature Visibility */ = { + isa = PBXGroup; + children = ( + BDFF03192BA39C5A00F324C9 /* NetworkProtectionFeatureVisibility.swift */, + BDFF031C2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift */, + BDFF03202BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift */, + ); + name = "Feature Visibility"; + sourceTree = ""; + }; C14882D627F2010700D59F0C /* ImportExport */ = { isa = PBXGroup; children = ( @@ -4824,6 +4838,7 @@ EE41BD182A729E9C00546C57 /* NetworkProtectionInviteViewModelTests.swift */, 4BCD146C2B05DB09000B1E4C /* NetworkProtectionAccessControllerTests.swift */, EEC02C152B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift */, + BDFF03242BA3D92E00F324C9 /* NetworkProtectionFeatureVisibilityTests.swift */, ); name = NetworkProtection; sourceTree = ""; @@ -5002,7 +5017,6 @@ F17669A21E411D63003D3222 /* Application */, 026F08B629B7DC130079B9DF /* AppTrackingProtection */, 981FED7222045FFA008488D7 /* AutoClear */, - 85E5602D26541D0900F4DC44 /* AutoComplete */, 1E1D8B5B2994FF7800C96994 /* Autoconsent */, F40F843228C92B1C0081AE75 /* Autofill */, 98559FD0267099F400A83094 /* ContentBlocker */, @@ -5175,7 +5189,6 @@ F143C3251E4A9A0E00CFDE3A /* URLExtension.swift */, 1E4DCF4B27B6A4CB00961E25 /* URLFileExtension.swift */, F1075C911E9EF827006BE8A8 /* UserDefaultsExtension.swift */, - 98982B3322F8D8E400578AC9 /* Debounce.swift */, 1CB7B82023CEA1F800AA24EA /* DateExtension.swift */, 1E8AD1DA27C51AE000ABA377 /* TimeIntervalExtension.swift */, 85449EFA23FDA0BC00512AAF /* UserDefaultsPropertyWrapper.swift */, @@ -5208,8 +5221,6 @@ F15D43211E70849A00BF2CDC /* Autocomplete */ = { isa = PBXGroup; children = ( - F17922D31E7109C4006E3D97 /* API */, - F17922DC1E717C91006E3D97 /* Domain */, F17922D41E7109DB006E3D97 /* UI */, ); name = Autocomplete; @@ -5262,14 +5273,6 @@ name = Mocks; sourceTree = ""; }; - F17922D31E7109C4006E3D97 /* API */ = { - isa = PBXGroup; - children = ( - F1A568391E70F98E0081082E /* AutocompleteRequest.swift */, - ); - name = API; - sourceTree = ""; - }; F17922D41E7109DB006E3D97 /* UI */ = { isa = PBXGroup; children = ( @@ -5278,18 +5281,11 @@ F17922DF1E71BB59006E3D97 /* AutocompleteViewControllerDelegate.swift */, F17922E11E71CD67006E3D97 /* NoSuggestionsTableViewCell.swift */, F17922DD1E7192E6006E3D97 /* SuggestionTableViewCell.swift */, + 8562CE142B9B645C00E1D399 /* CachedBookmarkSuggestions.swift */, ); name = UI; sourceTree = ""; }; - F17922DC1E717C91006E3D97 /* Domain */ = { - isa = PBXGroup; - children = ( - F17922DA1E717C8D006E3D97 /* Suggestion.swift */, - ); - name = Domain; - sourceTree = ""; - }; F17D722C1E8B3563003E8B0E /* Domain */ = { isa = PBXGroup; children = ( @@ -5964,6 +5960,7 @@ D61CDA152B7CF77300A0FBB9 /* Subscription */, D61CDA172B7CF78300A0FBB9 /* ZIPFoundation */, 858D009C2B9799FC004E5B4C /* History */, + 851F74252B9A1BFD00747C42 /* Suggestions */, ); productName = Core; productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */; @@ -6117,6 +6114,7 @@ files = ( 0262085C2A37915D006CB755 /* ios_blocklist_075.json in Resources */, CB1143DE2AF6D4B600C1CCD3 /* InfoPlist.strings in Resources */, + 4BCBE4602BA7E87100FC75A1 /* PrivacyInfo.xcprivacy in Resources */, EEDFE2DA2AC6ED4F00F0E19C /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6280,6 +6278,7 @@ 8512EA9D24EEA6820073EE19 /* Assets.xcassets in Resources */, 98B001AA251EABB40090EC07 /* Localizable.strings in Resources */, 98B001A4251EABB40090EC07 /* InfoPlist.strings in Resources */, + 4BCBE45E2BA7E81F00FC75A1 /* PrivacyInfo.xcprivacy in Resources */, 8512EA5724ED30D30073EE19 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6464,6 +6463,7 @@ files = ( 02025B0D29884D2C00E694E7 /* AppTrackerData.swift in Sources */, 4BEF656C2989C2FC00B650CB /* TunnelEvent.swift in Sources */, + BDFF03232BA3D8E300F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, 02025A9A2988229800E694E7 /* TUNInterface.swift in Sources */, 02025A9B2988229800E694E7 /* IPStackProtocol.swift in Sources */, 02025AA32988229800E694E7 /* PacketProtocolParser.swift in Sources */, @@ -6521,6 +6521,7 @@ 02025AEC2988229800E694E7 /* AppTrackingProtectionPacketTunnelProvider.swift in Sources */, 02025B1029884DC500E694E7 /* AppTrackerDataParser.swift in Sources */, 4BB697A52B1D99C5003699B5 /* VPNWaitlistActivationDateStore.swift in Sources */, + BDFF03212BA3D3CF00F324C9 /* NetworkProtectionVisibilityForTunnelProvider.swift in Sources */, EEFC6A602AC0F2F80065027D /* UserText.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -6584,6 +6585,7 @@ F4E1936625AF722F001D2666 /* HighlightCutOutView.swift in Sources */, 1E162605296840D80004127F /* Triangle.swift in Sources */, B609D5522862EAFF0088CAC2 /* InlineWKDownloadDelegate.swift in Sources */, + BDFF03222BA3D8E200F324C9 /* NetworkProtectionFeatureVisibility.swift in Sources */, B652DEFD287BE67400C12A9C /* UserScripts.swift in Sources */, 31DD208427395A5A008FB313 /* VoiceSearchHelper.swift in Sources */, 9874F9EE2187AFCE00CAF33D /* Themable.swift in Sources */, @@ -6626,6 +6628,7 @@ 853C5F6121C277C7001F7A05 /* global.swift in Sources */, EE9D68D82AE15AD600B55EF4 /* UIApplicationExtension.swift in Sources */, F13B4BD31F1822C700814661 /* Tab.swift in Sources */, + BDFF031D2BA3D2BD00F324C9 /* DefaultNetworkProtectionVisibility.swift in Sources */, F1BE54581E69DE1000FCF649 /* TutorialSettings.swift in Sources */, 1EE52ABB28FB1D6300B750C1 /* UIImageExtension.swift in Sources */, 858650D12469BCDE00C36F8A /* DaxDialogs.swift in Sources */, @@ -6846,7 +6849,6 @@ 0268FC132A449F04000EE6A2 /* OnboardingContainerView.swift in Sources */, 858650D9246B0D3C00C36F8A /* DaxOnboardingViewController.swift in Sources */, 312E5746283BB04A00C18FA0 /* AutofillEmptySearchView.swift in Sources */, - F1A5683A1E70F98E0081082E /* AutocompleteRequest.swift in Sources */, 8565A34B1FC8D96B00239327 /* LaunchTabNotification.swift in Sources */, 0290472829E861BE0008FE3C /* AppTPTrackerDetailViewModel.swift in Sources */, 311BD1AD2836BB3900AEF6C1 /* AutofillItemsEmptyView.swift in Sources */, @@ -6913,8 +6915,8 @@ 851624C72B96389D002D5CD7 /* HistoryDebugViewController.swift in Sources */, 8540BBA22440857A00017FE4 /* PreserveLoginsWorker.swift in Sources */, 85DFEDF924CF3D0E00973FE7 /* TabsBarCell.swift in Sources */, - F17922DB1E717C8D006E3D97 /* Suggestion.swift in Sources */, 020108A729A6ABF600644F9D /* AppTPToggleView.swift in Sources */, + 8562CE152B9B645C00E1D399 /* CachedBookmarkSuggestions.swift in Sources */, 02A54A982A093126000C8FED /* AppTPHomeViewModel.swift in Sources */, C13F3F682B7F88100083BE40 /* AuthConfirmationPromptView.swift in Sources */, F1617C191E573EA800DEDCAF /* TabSwitcherDelegate.swift in Sources */, @@ -7055,7 +7057,6 @@ 31C138B227A4097800FFD4B2 /* DownloadTestsHelper.swift in Sources */, 1E1D8B5D2994FFE100C96994 /* AutoconsentMessageProtocolTests.swift in Sources */, 85C11E532090B23A00BFFEB4 /* UserDefaultsHomeRowReminderStorageTests.swift in Sources */, - 85E5603026541D9E00F4DC44 /* AutocompleteRequestTests.swift in Sources */, F1DA2F7D1EBCF23700313F51 /* ExternalUrlSchemeTests.swift in Sources */, F198D78E1E39762C0088DA8A /* StringExtensionTests.swift in Sources */, 31B1FA87286EFC5C00CA3C1C /* XCTestCaseExtension.swift in Sources */, @@ -7074,6 +7075,7 @@ 850559D223CF710C0055C0D5 /* WebCacheManagerTests.swift in Sources */, EEC02C162B065BE00045CE11 /* NetworkProtectionVPNLocationViewModelTests.swift in Sources */, 987130C5294AAB9F00AB05E0 /* BookmarkEditorViewModelTests.swift in Sources */, + BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, 8341D807212D5E8D000514C2 /* HashExtensionTest.swift in Sources */, C1D21E2F293A599C006E5A05 /* AutofillLoginSessionTests.swift in Sources */, 85D2187924BF6B8B004373D2 /* FaviconSourcesProviderTests.swift in Sources */, @@ -7251,7 +7253,6 @@ 37E615752A5F533E00ACD63D /* SyncCredentialsAdapter.swift in Sources */, 02CA904B24F6C11A00D41DDF /* NavigatorSharePatchUserScript.swift in Sources */, 85BDC3192436161C0053DB07 /* LoginFormDetectionUserScript.swift in Sources */, - 98982B3422F8D8E400578AC9 /* Debounce.swift in Sources */, 37DF000A29F9C416002B7D3E /* SyncMetadataDatabase.swift in Sources */, F143C3291E4A9A0E00CFDE3A /* URLExtension.swift in Sources */, F143C3271E4A9A0E00CFDE3A /* Logging.swift in Sources */, @@ -10190,6 +10191,11 @@ package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; productName = RemoteMessaging; }; + 851F74252B9A1BFD00747C42 /* Suggestions */ = { + isa = XCSwiftPackageProductDependency; + package = 98A16C2928A11BDE00A6C003 /* XCRemoteSwiftPackageReference "BrowserServicesKit" */; + productName = Suggestions; + }; 85875B6029912A9900115F05 /* SyncUI */ = { isa = XCSwiftPackageProductDependency; productName = SyncUI; diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 1b17628fd5..24ebd30421 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -140,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser", "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.0" + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" } }, { @@ -183,7 +183,7 @@ { "identity" : "trackerradarkit", "kind" : "remoteSourceControl", - "location" : "https://github.com/duckduckgo/TrackerRadarKit", + "location" : "https://github.com/duckduckgo/TrackerRadarKit.git", "state" : { "revision" : "a6b7ba151d9dc6684484f3785293875ec01cc1ff", "version" : "1.2.2" diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme index e564636928..d3926becad 100644 --- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme +++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo.xcscheme @@ -124,7 +124,7 @@ + isEnabled = "YES"> Void - - private let url: URL - private var task: URLSessionDataTask? - - init(query: String) throws { - self.url = try URL.makeAutocompleteURL(for: query) - } - - func execute(completion: @escaping Completion) { - var request = URLRequest.developerInitiated(url) - request.allHTTPHeaderFields = APIRequest.Headers().httpHeaders - - task = AutocompleteRequest.session.dataTask(with: request) { [weak self] (data, _, error) in - guard let weakSelf = self else { return } - do { - let suggestions = try weakSelf.processResult(data: data, error: error) - weakSelf.complete(completion, withSuccess: suggestions) - } catch { - weakSelf.complete(completion, withError: error) - } - } - task?.resume() - } - - private func processResult(data: Data?, error: Swift.Error?) throws -> [Suggestion] { - if let error = error { throw error } - guard let data = data else { throw Error.noData } - let entries = try JSONDecoder().decode([AutocompleteEntry].self, from: data) - - return entries.compactMap { - guard let phrase = $0.phrase else { return nil } - - if let isNav = $0.isNav { - // We definitely have a nav indication so use it. Phrase should be a fully qualified URL. - // Assume HTTP and that we'll auto-upgrade if needed. - let url = isNav ? URL(string: "http://\(phrase)") : nil - return Suggestion(source: .remote, suggestion: phrase, url: url) - } else { - // We need to infer nav based on the phrase to maintain previous behaviour (ie treat phrase that look like URLs like URLs) - let url = URL.webUrl(from: phrase) - return Suggestion(source: .remote, suggestion: phrase, url: url) - } - } - } - - private func complete(_ completion: @escaping Completion, withSuccess suggestions: [Suggestion]) { - DispatchQueue.main.async { - completion(suggestions, nil) - } - } - - private func complete(_ completion: @escaping Completion, withError error: Swift.Error) { - DispatchQueue.main.async { - completion(nil, error) - } - } - - func cancel() { - task?.cancel() - } -} diff --git a/DuckDuckGo/AutocompleteViewController.swift b/DuckDuckGo/AutocompleteViewController.swift index caa9e5b35a..8f57da2bb2 100644 --- a/DuckDuckGo/AutocompleteViewController.swift +++ b/DuckDuckGo/AutocompleteViewController.swift @@ -21,29 +21,42 @@ import Common import UIKit import Core import DesignResourcesKit +import Suggestions +import Networking +import CoreData +import Persistence +import History +import Combine class AutocompleteViewController: UIViewController { + private static let session = URLSession(configuration: .ephemeral) + struct Constants { - static let debounceDelay: TimeInterval = 0.1 + static let debounceDelay = 100 // millis static let minItems = 1 - static let maxLocalItems = 2 } weak var delegate: AutocompleteViewControllerDelegate? weak var presentationDelegate: AutocompleteViewControllerPresentationDelegate? - private var lastRequest: AutocompleteRequest? + private var task: URLSessionDataTask? + private var loader: SuggestionLoading? private var receivedResponse = false private var pendingRequest = false - fileprivate var query = "" + @Published fileprivate var query = "" + fileprivate var queryDebounceCancellable: AnyCancellable? + fileprivate var suggestions = [Suggestion]() fileprivate var selectedItem = -1 - private var bookmarksSearch: BookmarksStringSearch! - + private var historyCoordinator: HistoryCoordinating! + private var bookmarksDatabase: CoreDataDatabase! private var appSettings: AppSettings! + private lazy var cachedBookmarks: CachedBookmarks = { + CachedBookmarks(bookmarksDatabase) + }() var backgroundColor: UIColor { appSettings.currentAddressBarPosition.isBottom ? @@ -63,20 +76,19 @@ class AutocompleteViewController: UIViewController { } private var hidesBarsOnSwipeDefault = true - - private let debounce = Debounce(queue: .main, seconds: Constants.debounceDelay) @IBOutlet weak var tableView: UITableView! var shouldOffsetY = false - static func loadFromStoryboard(bookmarksSearch: BookmarksStringSearch, + static func loadFromStoryboard(bookmarksDatabase: CoreDataDatabase, + historyCoordinator: HistoryCoordinating, appSettings: AppSettings = AppDependencyProvider.shared.appSettings) -> AutocompleteViewController { let storyboard = UIStoryboard(name: "Autocomplete", bundle: nil) - guard let controller = storyboard.instantiateInitialViewController() as? AutocompleteViewController else { fatalError("Failed to instatiate correct Autocomplete view controller") } - controller.bookmarksSearch = bookmarksSearch + controller.bookmarksDatabase = bookmarksDatabase + controller.historyCoordinator = historyCoordinator controller.appSettings = appSettings return controller } @@ -85,6 +97,12 @@ class AutocompleteViewController: UIViewController { super.viewDidLoad() configureTableView() applyTheme(ThemeManager.shared.currentTheme) + + queryDebounceCancellable = $query + .debounce(for: .milliseconds(Constants.debounceDelay), scheduler: RunLoop.main) + .sink { [weak self] query in + self?.requestSuggestions(query: query) + } } private func configureTableView() { @@ -125,24 +143,29 @@ class AutocompleteViewController: UIViewController { } func updateQuery(query: String) { - self.query = query selectedItem = -1 cancelInFlightRequests() - debounce.schedule { [weak self] in - self?.requestSuggestions(query: query) - } + self.query = query } func willDismiss(with query: String) { - guard selectedItem != -1, selectedItem < suggestions.count else { return } - + guard suggestions.indices.contains(selectedItem) else { return } let suggestion = suggestions[selectedItem] - if let url = suggestion.url { - if query == url.absoluteString { - firePixel(selectedSuggestion: suggestion) - } - } else if query == suggestion.suggestion { - firePixel(selectedSuggestion: suggestion) + firePixelForSelectedSuggestion(suggestion) + } + + private func firePixelForSelectedSuggestion(_ suggestion: Suggestion) { + switch suggestion { + case .phrase: + Pixel.fire(pixel: .autocompleteClickPhrase) + case .website: + Pixel.fire(pixel: .autocompleteClickWebsite) + case .bookmark(_, _, isFavorite: let isFavorite, _): + Pixel.fire(pixel: isFavorite ? .autocompleteClickFavorite : .autocompleteClickBookmark) + case .historyEntry: + Pixel.fire(pixel: .autocompleteClickHistory) + case .unknown(value: let value): + assertionFailure("Unknown suggestion \(value)") } } @@ -152,47 +175,32 @@ class AutocompleteViewController: UIViewController { } private func cancelInFlightRequests() { - if let inFlightRequest = lastRequest { - inFlightRequest.cancel() - lastRequest = nil - } + task?.cancel() + task = nil } private func requestSuggestions(query: String) { selectedItem = -1 tableView.reloadData() - do { - lastRequest = try AutocompleteRequest(query: query) - pendingRequest = true - } catch { - os_log("Couldn‘t form AutocompleteRequest for query “%s”: %s", log: .lifecycleLog, type: .debug, query, error.localizedDescription) - lastRequest = nil - pendingRequest = false - return - } - lastRequest!.execute { [weak self] (suggestions, error) in - guard let strongSelf = self else { return } - - Task { @MainActor in - let matches = strongSelf.bookmarksSearch.search(query: query) - let notQueryMatches = matches.filter { $0.url.absoluteString != query } - let filteredMatches = notQueryMatches.prefix(Constants.maxLocalItems) - let localSuggestions = filteredMatches.map { Suggestion(source: .local, - suggestion: $0.title, - url: $0.url) - } - - guard let suggestions = suggestions, error == nil else { - os_log("%s", log: .generalLog, type: .debug, error?.localizedDescription ?? "Failed to retrieve suggestions") - self?.updateSuggestions(localSuggestions) - return - } - - let combinedSuggestions = localSuggestions + suggestions - strongSelf.updateSuggestions(Array(combinedSuggestions)) - strongSelf.pendingRequest = false + loader = SuggestionLoader(dataSource: self, urlFactory: { phrase in + guard let url = URL(trimmedAddressBarString: phrase), + let scheme = url.scheme, + scheme.description.hasPrefix("http"), + url.isValid else { + return nil + } + + return url + }) + pendingRequest = true + + loader?.getSuggestions(query: query) { [weak self] result, error in + defer { + self?.pendingRequest = false } + guard error == nil else { return } + self?.updateSuggestions(result?.all ?? []) } } @@ -262,37 +270,29 @@ extension AutocompleteViewController: UITableViewDataSource { if appSettings.currentAddressBarPosition.isBottom && suggestions.isEmpty { return view.frame.height } - return 46 + + let defaultHeight: CGFloat = 46 + guard suggestions.indices.contains(indexPath.row) else { return defaultHeight } + + switch suggestions[indexPath.row] { + case .bookmark, .historyEntry: + return 60 + default: + return defaultHeight + } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return receivedResponse ? max(Constants.minItems, suggestions.count) : 0 } - - private func firePixel(selectedSuggestion: Suggestion) { - let resultsIncludeBookmarks: Bool - if let firstSuggestion = suggestions.first { - resultsIncludeBookmarks = firstSuggestion.source == .local - } else { - resultsIncludeBookmarks = false - } - - let params = [PixelParameters.autocompleteBookmarkCapable: bookmarksSearch.hasData ? "true" : "false", - PixelParameters.autocompleteIncludedLocalResults: resultsIncludeBookmarks ? "true" : "false"] - - if selectedSuggestion.source == .local { - Pixel.fire(pixel: .autocompleteSelectedLocal, withAdditionalParameters: params) - } else { - Pixel.fire(pixel: .autocompleteSelectedRemote, withAdditionalParameters: params) - } - } + } extension AutocompleteViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let suggestion = suggestions[indexPath.row] - firePixel(selectedSuggestion: suggestion) delegate?.autocomplete(selectedSuggestion: suggestion) + firePixelForSelectedSuggestion(suggestion) } } @@ -334,3 +334,37 @@ extension AutocompleteViewController { } } + +extension AutocompleteViewController: SuggestionLoadingDataSource { + + func history(for suggestionLoading: Suggestions.SuggestionLoading) -> [HistorySuggestion] { + return historyCoordinator.history ?? [] + } + + func bookmarks(for suggestionLoading: Suggestions.SuggestionLoading) -> [Suggestions.Bookmark] { + return cachedBookmarks.all + } + + func suggestionLoading(_ suggestionLoading: Suggestions.SuggestionLoading, suggestionDataFromUrl url: URL, withParameters parameters: [String: String], completion: @escaping (Data?, Error?) -> Void) { + var queryURL = url + parameters.forEach { + queryURL = queryURL.appendingParameter(name: $0.key, value: $0.value) + } + + var request = URLRequest.developerInitiated(queryURL) + request.allHTTPHeaderFields = APIRequest.Headers().httpHeaders + task = Self.session.dataTask(with: request) { data, _, error in + completion(data, error) + } + task?.resume() + } + +} + +extension HistoryEntry: HistorySuggestion { + + public var numberOfVisits: Int { + return numberOfTotalVisits + } + +} diff --git a/DuckDuckGo/AutocompleteViewControllerDelegate.swift b/DuckDuckGo/AutocompleteViewControllerDelegate.swift index fbb0b6d9fb..e7f3671328 100644 --- a/DuckDuckGo/AutocompleteViewControllerDelegate.swift +++ b/DuckDuckGo/AutocompleteViewControllerDelegate.swift @@ -18,6 +18,7 @@ // import UIKit +import Suggestions protocol AutocompleteViewControllerDelegate: AnyObject { diff --git a/DuckDuckGo/Base.lproj/Autocomplete.storyboard b/DuckDuckGo/Base.lproj/Autocomplete.storyboard index 3254452129..2e7d24e4c4 100644 --- a/DuckDuckGo/Base.lproj/Autocomplete.storyboard +++ b/DuckDuckGo/Base.lproj/Autocomplete.storyboard @@ -1,9 +1,10 @@ - + - + + @@ -20,7 +21,7 @@ - + @@ -34,13 +35,28 @@ + + + + - + + + + + + +