From 98203852a9879b8af4815efcaefbd1fca89797d7 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Mon, 18 Mar 2024 12:53:34 +0000 Subject: [PATCH 1/8] Use History in Suggestions on iOS (#2552) Task/Issue URL: https://app.asana.com/0/0/1206524433066958/f Tech Design URL: CC: Description: Surfaces history in the autocomplete. Steps to test this PR: Perform a search Start typing part of the search, it should appear as a history item (with the clock icon) at the bottom of suggestions Browse to some sites Start typing part of some of the site titles, they should appear as history items at the bottom of the suggestions Visit a couple of pages several times (ie more then 3) Start typing part of those page titles, they should appear as history items at the top Add bookmarks and favourites and try again. Appropriate suggestions should be made. Use the fire button, history should no longer appear in suggestions. --- Core/BookmarksCachingSearch.swift | 52 +++-- Core/Debounce.swift | 39 ---- Core/DefaultVariantManager.swift | 4 + Core/HistoryCapture.swift | 9 +- Core/PixelEvent.swift | 24 ++- DuckDuckGo.xcodeproj/project.pbxproj | 54 ++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../xcschemes/DuckDuckGo.xcscheme | 2 +- DuckDuckGo/AutocompleteRequest.swift | 95 --------- DuckDuckGo/AutocompleteViewController.swift | 188 +++++++++++------- .../AutocompleteViewControllerDelegate.swift | 1 + DuckDuckGo/Base.lproj/Autocomplete.storyboard | 48 +++-- DuckDuckGo/BlankSnapshotViewController.swift | 1 + DuckDuckGo/CachedBookmarkSuggestions.swift | 61 ++++++ DuckDuckGo/MainViewController.swift | 68 +++++-- DuckDuckGo/OmniBarDelegate.swift | 1 + DuckDuckGo/SettingsCustomizeView.swift | 1 + DuckDuckGo/SettingsViewModel.swift | 10 +- DuckDuckGo/Suggestion.swift | 41 ---- DuckDuckGo/SuggestionTableViewCell.swift | 54 ++++- DuckDuckGo/SuggestionTrayViewController.swift | 17 +- DuckDuckGo/UserText.swift | 4 +- .../AutocompleteRequestTests.swift | 106 ---------- DuckDuckGoTests/HistoryCaptureTests.swift | 14 -- submodules/privacy-reference-tests | 2 +- 25 files changed, 409 insertions(+), 491 deletions(-) delete mode 100644 Core/Debounce.swift delete mode 100644 DuckDuckGo/AutocompleteRequest.swift create mode 100644 DuckDuckGo/CachedBookmarkSuggestions.swift delete mode 100644 DuckDuckGo/Suggestion.swift delete mode 100644 DuckDuckGoTests/AutocompleteRequestTests.swift 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/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..2f5a6d5e9e 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) @@ -677,9 +682,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 +1143,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" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index ef7ff84466..39c918f68a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -373,6 +373,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 */; }; @@ -424,6 +425,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 */; }; @@ -500,7 +502,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 */; }; @@ -589,7 +590,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 */; }; @@ -929,7 +929,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 */; }; @@ -941,7 +940,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 */; }; @@ -1538,6 +1536,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 = ""; }; @@ -1612,7 +1611,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 = ""; }; @@ -2201,7 +2199,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 = ""; }; @@ -2626,7 +2623,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 = ""; }; @@ -2639,7 +2635,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 = ""; }; @@ -2815,6 +2810,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 */, @@ -4122,14 +4118,6 @@ name = iPad; sourceTree = ""; }; - 85E5602D26541D0900F4DC44 /* AutoComplete */ = { - isa = PBXGroup; - children = ( - 85E5602E26541D1D00F4DC44 /* AutocompleteRequestTests.swift */, - ); - name = AutoComplete; - sourceTree = ""; - }; 85EE7F53224667C3000FE757 /* WebContainer */ = { isa = PBXGroup; children = ( @@ -4999,7 +4987,6 @@ F17669A21E411D63003D3222 /* Application */, 026F08B629B7DC130079B9DF /* AppTrackingProtection */, 981FED7222045FFA008488D7 /* AutoClear */, - 85E5602D26541D0900F4DC44 /* AutoComplete */, 1E1D8B5B2994FF7800C96994 /* Autoconsent */, F40F843228C92B1C0081AE75 /* Autofill */, 98559FD0267099F400A83094 /* ContentBlocker */, @@ -5172,7 +5159,6 @@ F143C3251E4A9A0E00CFDE3A /* URLExtension.swift */, 1E4DCF4B27B6A4CB00961E25 /* URLFileExtension.swift */, F1075C911E9EF827006BE8A8 /* UserDefaultsExtension.swift */, - 98982B3322F8D8E400578AC9 /* Debounce.swift */, 1CB7B82023CEA1F800AA24EA /* DateExtension.swift */, 1E8AD1DA27C51AE000ABA377 /* TimeIntervalExtension.swift */, 85449EFA23FDA0BC00512AAF /* UserDefaultsPropertyWrapper.swift */, @@ -5205,8 +5191,6 @@ F15D43211E70849A00BF2CDC /* Autocomplete */ = { isa = PBXGroup; children = ( - F17922D31E7109C4006E3D97 /* API */, - F17922DC1E717C91006E3D97 /* Domain */, F17922D41E7109DB006E3D97 /* UI */, ); name = Autocomplete; @@ -5259,14 +5243,6 @@ name = Mocks; sourceTree = ""; }; - F17922D31E7109C4006E3D97 /* API */ = { - isa = PBXGroup; - children = ( - F1A568391E70F98E0081082E /* AutocompleteRequest.swift */, - ); - name = API; - sourceTree = ""; - }; F17922D41E7109DB006E3D97 /* UI */ = { isa = PBXGroup; children = ( @@ -5275,18 +5251,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 = ( @@ -5961,6 +5930,7 @@ D61CDA152B7CF77300A0FBB9 /* Subscription */, D61CDA172B7CF78300A0FBB9 /* ZIPFoundation */, 858D009C2B9799FC004E5B4C /* History */, + 851F74252B9A1BFD00747C42 /* Suggestions */, ); productName = Core; productReference = F143C2E41E4A4CD400CFDE3A /* Core.framework */; @@ -6842,7 +6812,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 */, @@ -6908,8 +6877,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 */, @@ -7051,7 +7020,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 */, @@ -7247,7 +7215,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 */, @@ -10028,7 +9995,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 125.0.2; + version = 126.0.0; }; }; B6F997C22B8F374300476735 /* XCRemoteSwiftPackageReference "apple-toolbox" */ = { @@ -10186,6 +10153,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 f2336cc993..beb5810bd7 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "810bf41347ff437b5d0154405a238553537240a4", - "version" : "125.0.2" + "revision" : "7656e94efcf4eedf1c16152c63f57fb52b6ad079", + "version" : "126.0.0" } }, { 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 @@ + + + + - + + + + + + +