From e2aa256a9c807750a8b8951d3beacbddf98c074a Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 23 Aug 2022 12:30:24 -0700 Subject: [PATCH 01/11] Update the global accent color. (#695) Task/Issue URL: https://app.asana.com/0/1199178362774117/1202779407911255/f Tech Design URL: CC: Description: This PR updates the global accent color. The previous copy had transparency embedded into the color itself, and caused a lot of inconsistency issues. The color itself has been slightly tweaked per design's request. --- .../GlobalAccentColor.colorset/Contents.json | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json index 856b2fdda0..665220ff0c 100644 --- a/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json +++ b/DuckDuckGo/Assets.xcassets/Colors/GlobalAccentColor.colorset/Contents.json @@ -4,28 +4,10 @@ "color" : { "color-space" : "srgb", "components" : { - "alpha" : "0.600", - "blue" : "0xEF", - "green" : "0x69", - "red" : "0x39" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "contrast", - "value" : "high" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.600", - "blue" : "0.937", - "green" : "0.412", - "red" : "0.224" + "alpha" : "1.000", + "blue" : "0xFC", + "green" : "0x7A", + "red" : "0x3A" } }, "idiom" : "universal" From 3e19d5346bfcc81cb9fe8d478b95f558d3d6670a Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 23 Aug 2022 16:03:38 -0700 Subject: [PATCH 02/11] Remove email waitlist (#615) Task/Issue URL: https://app.asana.com/0/1199230911884351/1202144584700631/f Tech Design URL: CC: Description: This PR removes the email waitlist feature. As far as the macOS app is concerned, this change is pretty simple, since it never included any of the waitlist code. All that's done here is update the email URL. --- DuckDuckGo/Email/EmailUrlExtensions.swift | 13 ++++++++++--- .../Navigation Bar/View/MoreOptionsMenu.swift | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/DuckDuckGo/Email/EmailUrlExtensions.swift b/DuckDuckGo/Email/EmailUrlExtensions.swift index 3323b45a1c..d693c13289 100644 --- a/DuckDuckGo/Email/EmailUrlExtensions.swift +++ b/DuckDuckGo/Email/EmailUrlExtensions.swift @@ -23,13 +23,20 @@ extension EmailUrls { private struct Url { static let emailProtectionLink = "https://duckduckgo.com/email" - static let emailLandingPage = "https://duckduckgo.com/email/enable-autofill" static let emailGenerateTokenPage = "https://duckduckgo.com/email/new-address" static let emailAuthenticationHosts = ["quack.duckduckgo.com", "quackdev.duckduckgo.com"] } + + private struct DevUrl { + static let emailProtectionLink = "https://quackdev.duckduckgo.com/email" + } - var emailLandingPage: URL { - return URL(string: Url.emailLandingPage)! + var emailProtectionLink: URL { + #if DEBUG + return URL(string: DevUrl.emailProtectionLink)! + #else + return URL(string: Url.emailProtectionLink)! + #endif } func shouldAuthenticateWithEmailCredentials(url: URL) -> Bool { diff --git a/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift b/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift index 2ba9cea061..ebad6146d4 100644 --- a/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/Navigation Bar/View/MoreOptionsMenu.swift @@ -302,7 +302,7 @@ final class EmailOptionsButtonSubMenu: NSMenu { } @objc func turnOnEmailAction(_ sender: NSMenuItem) { - let tab = Tab(content: .url(EmailUrls().emailLandingPage)) + let tab = Tab(content: .url(EmailUrls().emailProtectionLink)) tabCollectionViewModel.append(tab: tab) Pixel.fire(.moreMenu(result: .emailProtection)) } From e261732fd5e5804fc1e02dcf30e60a263e1b84e4 Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Wed, 24 Aug 2022 11:50:34 +0100 Subject: [PATCH 03/11] update the menu once the favicons have loaded (#696) --- .../Favicons/Model/FaviconManager.swift | 3 +++ DuckDuckGo/Menus/MainMenu.swift | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/DuckDuckGo/Favicons/Model/FaviconManager.swift b/DuckDuckGo/Favicons/Model/FaviconManager.swift index 5ad58fbdf4..05d2d960df 100644 --- a/DuckDuckGo/Favicons/Model/FaviconManager.swift +++ b/DuckDuckGo/Favicons/Model/FaviconManager.swift @@ -48,6 +48,8 @@ final class FaviconManager: FaviconManagement { private let faviconURLSession = URLSession(configuration: .ephemeral) + @Published var faviconsLoaded = false + func loadFavicons() { imageCache.loadFavicons { _ in self.imageCache.cleanOldExcept(fireproofDomains: FireproofDomains.shared, @@ -55,6 +57,7 @@ final class FaviconManager: FaviconManagement { self.referenceCache.loadReferences { _ in self.referenceCache.cleanOldExcept(fireproofDomains: FireproofDomains.shared, bookmarkManager: LocalBookmarkManager.shared) + self.faviconsLoaded = true } } } diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 22831a7b6c..9c82d6aaab 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -123,10 +123,31 @@ final class MainMenu: NSMenu { #endif subscribeToBookmarkList() + subscribeToFavicons() } // MARK: - Bookmarks + var faviconsCancellable: AnyCancellable? + private func subscribeToFavicons() { + faviconsCancellable = FaviconManager.shared.$faviconsLoaded + .receive(on: DispatchQueue.main).sink(receiveValue: { [weak self] loaded in + if loaded { + self?.updateFavicons(self?.bookmarksMenuItem) + self?.updateFavicons(self?.favoritesMenuItem) + } + }) + } + + private func updateFavicons(_ menuItem: NSMenuItem?) { + if let bookmark = menuItem?.representedObject as? Bookmark { + menuItem?.image = BookmarkViewModel(entity: bookmark).menuFavicon + } + menuItem?.submenu?.items.forEach { menuItem in + updateFavicons(menuItem) + } + } + var bookmarkListCancellable: AnyCancellable? private func subscribeToBookmarkList() { bookmarkListCancellable = LocalBookmarkManager.shared.$list From e370771cfe379a311172913f7e3040d322d4aa68 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 26 Aug 2022 10:40:12 +0200 Subject: [PATCH 04/11] Update for BSK changes of URL parameter API (#697) * Replace addParameter with appendingParameters * Set BSK version to 26.0.0 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- DuckDuckGo/API/APIRequest.swift | 2 +- .../ContentOverlayViewController.swift | 8 +------ .../Common/Extensions/URLExtension.swift | 21 +++++++++++-------- .../Email/EmailManagerRequestDelegate.swift | 8 +------ .../View/AddressBarTextField.swift | 4 ++-- .../Model/SuggestionContainer.swift | 6 +++--- 7 files changed, 21 insertions(+), 30 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2e85fd1a32..72f5b2a495 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -6212,7 +6212,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 24.0.0; + version = 26.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo/API/APIRequest.swift b/DuckDuckGo/API/APIRequest.swift index 447d8ecc47..fa3d4c71fd 100644 --- a/DuckDuckGo/API/APIRequest.swift +++ b/DuckDuckGo/API/APIRequest.swift @@ -105,7 +105,7 @@ enum APIRequest { headers: HTTPHeaders = APIHeaders().defaultHeaders, timeoutInterval: TimeInterval = 60.0) -> URLRequest { let url = (try? parameters?.reduce(url) { partialResult, parameter in - try partialResult.addParameter( + try partialResult.appendingParameter( name: parameter.key, value: parameter.value, allowedReservedCharacters: allowedQueryReservedCharacters diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index dd8297c59d..573ed9ff6f 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -147,13 +147,7 @@ public final class ContentOverlayViewController: NSViewController, EmailManagerR completion: @escaping (Data?, Error?) -> Void) { let currentQueue = OperationQueue.current - let finalURL: URL - - if let parameters = parameters { - finalURL = (try? url.addParameters(parameters)) ?? url - } else { - finalURL = url - } + let finalURL = (try? url.appendingParameters(parameters ?? [:])) ?? url var request = URLRequest(url: finalURL, timeoutInterval: timeoutInterval) request.allHTTPHeaderFields = headers diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 34a6046610..068b2f4167 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -45,9 +45,8 @@ extension URL { } do { - var searchUrl = Self.duckDuckGo - searchUrl = try searchUrl.addParameter(name: DuckDuckGoParameters.search.rawValue, value: trimmedQuery) - return searchUrl + return try Self.duckDuckGo + .appendingParameter(name: DuckDuckGoParameters.search.rawValue, value: trimmedQuery) } catch let error { os_log("URL extension: %s", type: .error, error.localizedDescription) return nil @@ -224,20 +223,24 @@ extension URL { static func searchAtb(atbWithVariant: String, setAtb: String) -> URL? { return try? Self.initialAtb - .addParameter(name: DuckDuckGoParameters.ATB.atb, value: atbWithVariant) - .addParameter(name: DuckDuckGoParameters.ATB.setAtb, value: setAtb) + .appendingParameters([ + DuckDuckGoParameters.ATB.atb: atbWithVariant, + DuckDuckGoParameters.ATB.setAtb: setAtb + ]) } static func appRetentionAtb(atbWithVariant: String, setAtb: String) -> URL? { return try? Self.initialAtb - .addParameter(name: DuckDuckGoParameters.ATB.activityType, value: DuckDuckGoParameters.ATB.appUsageValue) - .addParameter(name: DuckDuckGoParameters.ATB.atb, value: atbWithVariant) - .addParameter(name: DuckDuckGoParameters.ATB.setAtb, value: setAtb) + .appendingParameters([ + DuckDuckGoParameters.ATB.activityType: DuckDuckGoParameters.ATB.appUsageValue, + DuckDuckGoParameters.ATB.atb: atbWithVariant, + DuckDuckGoParameters.ATB.setAtb: setAtb + ]) } static func exti(forAtb atb: String) -> URL? { let extiUrl = URL(string: Self.exti)! - return try? extiUrl.addParameter(name: DuckDuckGoParameters.ATB.atb, value: atb) + return try? extiUrl.appendingParameter(name: DuckDuckGoParameters.ATB.atb, value: atb) } // MARK: - Components diff --git a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift index 0e163b11bd..a107ff76e9 100644 --- a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift +++ b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift @@ -31,13 +31,7 @@ extension EmailManagerRequestDelegate { completion: @escaping (Data?, Error?) -> Void) { let currentQueue = OperationQueue.current - let finalURL: URL - - if let parameters = parameters { - finalURL = (try? url.addParameters(parameters)) ?? url - } else { - finalURL = url - } + let finalURL = (try? url.appendingParameters(parameters ?? [:])) ?? url var request = URLRequest(url: finalURL, timeoutInterval: timeoutInterval) request.allHTTPHeaderFields = headers diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift index c1d99f79f2..5fadec43b2 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift @@ -265,11 +265,11 @@ final class AddressBarTextField: NSTextField { let oldURL = selectedTabViewModel.tab.content.url, oldURL.isDuckDuckGoSearch { if let ia = try? oldURL.getParameter(name: URL.DuckDuckGoParameters.ia.rawValue), - let newURL = try? url.addParameter(name: URL.DuckDuckGoParameters.ia.rawValue, value: ia) { + let newURL = try? url.appendingParameter(name: URL.DuckDuckGoParameters.ia.rawValue, value: ia) { url = newURL } if let iax = try? oldURL.getParameter(name: URL.DuckDuckGoParameters.iax.rawValue), - let newURL = try? url.addParameter(name: URL.DuckDuckGoParameters.iax.rawValue, value: iax) { + let newURL = try? url.appendingParameter(name: URL.DuckDuckGoParameters.iax.rawValue, value: iax) { url = newURL } } diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift index 5bec9fc19a..d10635e34b 100644 --- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift +++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift @@ -99,9 +99,9 @@ extension SuggestionContainer: SuggestionLoadingDataSource { completion: @escaping (Data?, Error?) -> Void) { var url = url parameters.forEach { - if let newUrl = try? url.addParameter(name: $0.key, value: $0.value) { - url = newUrl - } else { + do { + try url = url.appendingParameter(name: $0.key, value: $0.value) + } catch { assertionFailure("SuggestionContainer: Failed to add parameter") } } From be33832e72841ce163b1672d107acd8b1e07fe91 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 26 Aug 2022 10:59:44 +0200 Subject: [PATCH 05/11] Update to use URL constructors and helpers from BSK (#698) Task/Issue URL: https://app.asana.com/0/0/1202861341670740/f Description: Update to use URL constructors and helpers from BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- DuckDuckGo/API/APIRequest.swift | 2 +- DuckDuckGo/App Delegate/CopyHandler.swift | 2 +- .../ViewModel/BookmarkViewModel.swift | 2 +- DuckDuckGo/Browser Tab/Model/Tab.swift | 2 +- .../Browser Tab/ViewModel/TabViewModel.swift | 2 +- .../ViewModel/WebViewStateObserver.swift | 4 +- .../Extensions/FileManagerExtension.swift | 2 +- .../Common/Extensions/StringExtension.swift | 57 +------ .../Common/Extensions/URLExtension.swift | 153 ++---------------- .../Common/View/SwiftUI/FaviconView.swift | 4 +- .../Chromium/ImportedBookmarks.swift | 2 +- .../Bookmarks/HTML/BookmarkHTMLReader.swift | 2 +- .../Model/FaviconReferenceCache.swift | 4 +- .../View/FeedbackViewController.swift | 2 +- .../Model/FileDownloadManager.swift | 2 +- DuckDuckGo/Fire/Model/Fire.swift | 2 +- .../FireproofingURLExtensions.swift | 3 +- .../Fireproofing/Model/FireproofDomains.swift | 6 +- .../Model/FireproofDomainsContainer.swift | 6 +- .../View/FireproofInfoViewController.swift | 2 +- DuckDuckGo/History/Model/HistoryEntry.swift | 2 +- .../Model/HomePageRecentlyVisitedModel.swift | 10 +- .../View/AddressBarTextField.swift | 6 +- .../Permissions/Model/PermissionManager.swift | 8 +- .../Permissions/Model/PermissionModel.swift | 2 +- .../Permissions/Model/PermissionType.swift | 2 +- .../View/PermissionContextMenu.swift | 2 +- .../Pinned Tabs/Model/PinnedTabsManager.swift | 2 +- .../Pinned Tabs/View/PinnedTabView.swift | 4 +- .../Model/DefaultBrowserPreferences.swift | 2 +- .../Model/PreferencesSection.swift | 2 +- .../View/FireproofDomainsViewController.swift | 4 +- .../PasswordManagementItemListModel.swift | 2 +- .../Model/PasswordManagementLoginModel.swift | 4 +- .../PasswordManagementLoginItemView.swift | 4 +- .../PasswordManagementViewController.swift | 4 +- .../View/SaveCredentialsViewController.swift | 4 +- DuckDuckGo/User Agent/Model/UserAgent.swift | 1 + .../Common/Extensions/URLExtensionTests.swift | 97 ----------- .../Permissions/PermissionManagerMock.swift | 6 +- .../Permissions/PermissionManagerTests.swift | 8 +- 42 files changed, 82 insertions(+), 357 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 72f5b2a495..08b82b7ef1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -6212,7 +6212,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 26.0.0; + version = 27.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo/API/APIRequest.swift b/DuckDuckGo/API/APIRequest.swift index fa3d4c71fd..2ed065349d 100644 --- a/DuckDuckGo/API/APIRequest.swift +++ b/DuckDuckGo/API/APIRequest.swift @@ -90,7 +90,7 @@ enum APIRequest { var etag = httpResponse?.headerValue(for: APIHeaders.Name.etag) // Handle weak etags - etag = etag?.drop(prefix: "W/") + etag = etag?.dropping(prefix: "W/") completion(Response(data: data, etag: etag, urlResponse: response), nil) } } diff --git a/DuckDuckGo/App Delegate/CopyHandler.swift b/DuckDuckGo/App Delegate/CopyHandler.swift index 7dc525dcdd..384c9b6b99 100644 --- a/DuckDuckGo/App Delegate/CopyHandler.swift +++ b/DuckDuckGo/App Delegate/CopyHandler.swift @@ -33,7 +33,7 @@ final class CopyHandler: NSObject { NSPasteboard.general.clearContents() NSPasteboard.general.setString(selectedText, forType: .string) - if let urlString = URL(trimmedAddressBarString: selectedText.trimmingWhitespaces())?.absoluteString, + if let urlString = URL(trimmedAddressBarString: selectedText.trimmingWhitespace())?.absoluteString, urlString == selectedText { NSPasteboard.general.setString(urlString, forType: .URL) } diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift index 5aeffadb88..dee54558e9 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarkViewModel.swift @@ -80,7 +80,7 @@ struct BookmarkViewModel { preconditionFailure("\(#file): Attempted to provide representing character for non-Bookmark") } - return bookmark.url.host?.dropWWW().first?.uppercased() ?? "-" + return bookmark.url.host?.droppingWwwPrefix().first?.uppercased() ?? "-" } } diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index e71e5af7e0..7c26e75c81 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -692,7 +692,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { // Add to local history if let host = url.host, !host.isEmpty { - localHistory.insert(host.dropWWW()) + localHistory.insert(host.droppingWwwPrefix()) } } diff --git a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift index e85c8710d1..d2331282f5 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/TabViewModel.swift @@ -194,7 +194,7 @@ final class TabViewModel { guard !errorViewState.isVisible else { let failingUrl = tab.error?.failingUrl addressBarString = failingUrl?.absoluteString ?? "" - passiveAddressBarString = failingUrl?.host?.drop(prefix: URL.HostPrefix.www.separated()) ?? "" + passiveAddressBarString = failingUrl?.host?.droppingWwwPrefix() ?? "" return } diff --git a/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift b/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift index f8bd0b2296..eebb360929 100644 --- a/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift +++ b/DuckDuckGo/Browser Tab/ViewModel/WebViewStateObserver.swift @@ -117,8 +117,8 @@ final class WebViewStateObserver: NSObject { } private func updateTitle() { - if webView?.title?.trimmingWhitespaces().isEmpty ?? true { - tabViewModel?.tab.title = webView?.url?.host?.dropWWW() + if webView?.title?.trimmingWhitespace().isEmpty ?? true { + tabViewModel?.tab.title = webView?.url?.host?.droppingWwwPrefix() return } diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift index ba03fa0299..984ca55539 100644 --- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift +++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift @@ -53,7 +53,7 @@ extension FileManager { } let ownerDirectory = destURL.deletingLastPathComponent() - let fileNameWithoutExtension = destURL.lastPathComponent.drop(suffix: suffix) + let fileNameWithoutExtension = destURL.lastPathComponent.dropping(suffix: suffix) for copy in 0... { let destURL: URL = { diff --git a/DuckDuckGo/Common/Extensions/StringExtension.swift b/DuckDuckGo/Common/Extensions/StringExtension.swift index 14701a9b39..7114d55f33 100644 --- a/DuckDuckGo/Common/Extensions/StringExtension.swift +++ b/DuckDuckGo/Common/Extensions/StringExtension.swift @@ -18,31 +18,12 @@ import Foundation import os.log - -typealias RegEx = NSRegularExpression - -func regex(_ pattern: String, _ options: NSRegularExpression.Options = []) -> NSRegularExpression { - // swiftlint:disable force_try - return try! NSRegularExpression(pattern: pattern, options: options) - // swiftlint:enable force_try -} - -private extension RegEx { - // from https://stackoverflow.com/a/25717506/73479 - static let hostName = regex("^(((?!-)[A-Za-z0-9-]{1,63}(? String { - trimmingCharacters(in: .whitespacesAndNewlines) - } - func nsRange(from range: Range? = nil) -> NSRange { if let range = range { return NSRange(location: self[.. Bool { - let matches = regex.matches(in: self, options: .anchored, range: NSRange(location: 0, length: self.utf16.count)) - return matches.count == 1 - } - // MARK: - URL var url: URL? { @@ -75,31 +49,12 @@ extension String { static let localhost = "localhost" - var isValidHost: Bool { - return isValidHostname || isValidIpHost - } - - var isValidHostname: Bool { - if self == Self.localhost { - return true - } - return matches(.hostName) - } - - var isValidIpHost: Bool { - return matches(.ipAddress) - } - func dropSubdomain() -> String? { let parts = components(separatedBy: ".") guard parts.count > 1 else { return nil } return parts.dropFirst().joined(separator: ".") } - func dropWWW() -> String { - self.drop(prefix: URL.HostPrefix.www.separated()) - } - static func uniqueFilename(for fileType: UTType? = nil) -> String { let fileName = UUID().uuidString @@ -122,14 +77,4 @@ extension String { return hasPrefix(other) || other.hasPrefix(self) } - func drop(prefix: String) -> String { - return hasPrefix(prefix) ? String(dropFirst(prefix.count)) : self - } - - // MARK: - Suffix - - func drop(suffix: String) -> String { - return hasSuffix(suffix) ? String(dropLast(suffix.count)) : self - } - } diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index 068b2f4167..4253ab0d25 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -20,6 +20,14 @@ import Foundation import os.log import BrowserServicesKit +extension URL.NavigationalScheme { + + static var validSchemes: [URL.NavigationalScheme] { + return [.http, .https, .file] + } + +} + extension URL { // MARK: - Local @@ -54,7 +62,7 @@ extension URL { } static func makeURL(from addressBarString: String) -> URL? { - let trimmed = addressBarString.trimmingWhitespaces() + let trimmed = addressBarString.trimmingWhitespace() if let addressBarUrl = URL(trimmedAddressBarString: trimmed), addressBarUrl.isValid { return addressBarUrl @@ -68,105 +76,6 @@ extension URL { return nil } - /// URL and URLComponents can't cope with emojis and international characters so this routine does some manual processing while trying to - /// retain the input as much as possible. - init?(trimmedAddressBarString: String) { - var s = trimmedAddressBarString - - // Creates URL even if user enters one slash "/" instead of two slashes "//" after the hypertext scheme component - if let scheme = NavigationalScheme.hypertextSchemes.first(where: { s.hasPrefix($0.rawValue + ":/") }), - !s.hasPrefix(scheme.separated()) { - s = scheme.separated() + s.dropFirst(scheme.separated().count - 1) - } - - if let url = URL(string: s) { - // if URL has domain:port or user:password@domain mistakengly interpreted as a scheme - if let urlWithScheme = URL(string: NavigationalScheme.http.separated() + s), - urlWithScheme.port != nil || urlWithScheme.user != nil { - // could be a local domain but user needs to use the protocol to specify that - // make exception for "localhost" - guard urlWithScheme.host?.contains(".") == true || urlWithScheme.host == .localhost else { return nil } - self = urlWithScheme - return - - } else if url.scheme != nil { - self = url - return - - } else if let hostname = s.split(separator: "/").first { - guard hostname.contains(".") || String(hostname) == .localhost else { - // could be a local domain but user needs to use the protocol to specify that - return nil - } - } else { - return nil - } - - s = NavigationalScheme.http.separated() + s - } - - self.init(punycodeEncodedString: s) - } - - private init?(punycodeEncodedString: String) { - var s = punycodeEncodedString - let scheme: String - - if s.hasPrefix(URL.NavigationalScheme.http.separated()) { - scheme = URL.NavigationalScheme.http.separated() - } else if s.hasPrefix(URL.NavigationalScheme.https.separated()) { - scheme = URL.NavigationalScheme.https.separated() - } else if !s.contains(".") { - return nil - } else { - scheme = URL.NavigationalScheme.http.separated() - s = scheme + s - } - - let urlAndQuery = s.split(separator: "?") - guard !urlAndQuery.isEmpty, !urlAndQuery[0].contains(" ") else { - return nil - } - - var query = "" - if urlAndQuery.count > 1 { - // replace spaces with %20 in query values - do { - struct Throwable: Error {} - query = try "?" + urlAndQuery[1].components(separatedBy: "&").map { component in - try component.components(separatedBy: "=").enumerated().map { (idx, component) -> String in - if idx == 0 { // name - // don't allow spaces in query names - guard !component.contains(" ") else { throw Throwable() } - return component - } else { // value - return component.replacingOccurrences(of: " ", with: "%20") - } - }.joined(separator: "=") - }.joined(separator: "&") - } catch { - return nil - } - } - - let componentsWithoutQuery = urlAndQuery[0].split(separator: "/").dropFirst().map(String.init) - guard !componentsWithoutQuery.isEmpty else { - return nil - } - - let host = componentsWithoutQuery[0].punycodeEncodedHostname - - let encodedPath = componentsWithoutQuery - .dropFirst() - .map { $0.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlPathAllowed) ?? $0 } - .joined(separator: "/") - - let hostPathSeparator = !encodedPath.isEmpty || urlAndQuery[0].hasSuffix("/") ? "/" : "" - let url = scheme + host + hostPathSeparator + encodedPath + query - - self.init(string: url) - } - static func makeURL(fromSuggestionPhrase phrase: String) -> URL? { guard let url = URL(trimmedAddressBarString: phrase), url.isValid else { return nil } return url @@ -245,28 +154,6 @@ extension URL { // MARK: - Components - struct NavigationalScheme: RawRepresentable, Hashable { - let rawValue: String - - static let separator = "://" - - static let http = NavigationalScheme(rawValue: "http") - static let https = NavigationalScheme(rawValue: "https") - static let file = NavigationalScheme(rawValue: "ftp") - - func separated() -> String { - self.rawValue + Self.separator - } - - static var hypertextSchemes: [NavigationalScheme] { - return [.http, .https] - } - - static var validSchemes: [NavigationalScheme] { - return [.http, .https, .file] - } - } - enum HostPrefix: String { case www @@ -298,7 +185,7 @@ extension URL { (.none, _): break case (.some(false), true): - host = host.drop(prefix: HostPrefix.www.separated()) + host = host.dropping(prefix: HostPrefix.www.separated()) case (.some(true), false): host = HostPrefix.www.separated() + host } @@ -315,7 +202,7 @@ extension URL { let schemeRange = components.rangeOfScheme { string.replaceSubrange(schemeRange, with: "") if string.hasPrefix(URL.NavigationalScheme.separator) { - string = string.drop(prefix: URL.NavigationalScheme.separator) + string = string.dropping(prefix: URL.NavigationalScheme.separator) } } @@ -329,12 +216,12 @@ extension URL { func toString(forUserInput input: String, decodePunycode: Bool = true) -> String { let hasInputScheme = input.hasOrIsPrefix(of: self.separatedScheme ?? "") - let hasInputWww = input.drop(prefix: self.separatedScheme ?? "").hasOrIsPrefix(of: URL.HostPrefix.www.rawValue) + let hasInputWww = input.dropping(prefix: self.separatedScheme ?? "").hasOrIsPrefix(of: URL.HostPrefix.www.rawValue) let hasInputHost = (decodePunycode ? host?.idnaDecoded : host)?.hasOrIsPrefix(of: input) ?? false return self.toString(decodePunycode: decodePunycode, dropScheme: input.isEmpty || !(hasInputScheme && !hasInputHost), - needsWWW: !input.drop(prefix: self.separatedScheme ?? "").isEmpty + needsWWW: !input.dropping(prefix: self.separatedScheme ?? "").isEmpty && hasInputWww && !hasInputHost, dropTrailingSlash: !input.hasSuffix("/")) @@ -350,7 +237,7 @@ extension URL { filename = url.lastPathComponent } else { - filename = url.host?.dropWWW().replacingOccurrences(of: ".", with: "_") ?? "" + filename = url.host?.droppingWwwPrefix().replacingOccurrences(of: ".", with: "_") ?? "" } guard !filename.isEmpty else { return nil } @@ -364,18 +251,6 @@ extension URL { // MARK: - Validity - var isValid: Bool { - guard let scheme = scheme.map(NavigationalScheme.init) else { return false } - - if NavigationalScheme.hypertextSchemes.contains(scheme) { - return host?.isValidHost == true && user == nil - } - - // This effectively allows file:// and External App Scheme URLs to be entered by user - // Without this check single word entries get treated like domains - return true - } - var isDataURL: Bool { return scheme == "data" } diff --git a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift index 433c55413c..44846e89df 100644 --- a/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift +++ b/DuckDuckGo/Common/View/SwiftUI/FaviconView.swift @@ -57,8 +57,8 @@ struct FaviconView: View { ZStack { Rectangle() - .foregroundColor(Color.forDomain(domain.dropWWW())) - Text(String(domain.dropWWW().capitalized.first ?? "?")) + .foregroundColor(Color.forDomain(domain.droppingWwwPrefix())) + Text(String(domain.droppingWwwPrefix().capitalized.first ?? "?")) .font(.title) .foregroundColor(Color.white) } diff --git a/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift b/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift index 2aa4a4a2c4..a1778f9f79 100644 --- a/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift +++ b/DuckDuckGo/Data Import/Bookmarks/Chromium/ImportedBookmarks.swift @@ -77,7 +77,7 @@ struct ImportedBookmarks: Decodable { } init(name: String, type: String, urlString: String?, children: [BookmarkOrFolder]?, isDDGFavorite: Bool = false) { - self.name = name.trimmingWhitespaces() + self.name = name.trimmingWhitespace() self.type = type self.urlString = urlString self.children = children diff --git a/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift b/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift index 1909b62da0..4173e38057 100644 --- a/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift +++ b/DuckDuckGo/Data Import/Bookmarks/HTML/BookmarkHTMLReader.swift @@ -338,7 +338,7 @@ private extension XMLNode { } var text: String? { - stringValue?.trimmingWhitespaces() + stringValue?.trimmingWhitespace() } var bookmark: ImportedBookmarks.BookmarkOrFolder? { diff --git a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift index adc18a3e50..a825e5b6f0 100644 --- a/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift +++ b/DuckDuckGo/Favicons/Model/FaviconReferenceCache.swift @@ -113,7 +113,7 @@ final class FaviconReferenceCache { } } else if let host = documentURL.host, let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? - hostReferences[host.dropWWW()] : hostReferences["www.\(host)"]) { + hostReferences[host.droppingWwwPrefix()] : hostReferences["www.\(host)"]) { switch sizeCategory { case .small: return hostCacheEntry.smallFaviconUrl ?? hostCacheEntry.mediumFaviconUrl default: return hostCacheEntry.mediumFaviconUrl @@ -128,7 +128,7 @@ final class FaviconReferenceCache { return nil } - let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? hostReferences[host.dropWWW()] : hostReferences["www.\(host)"]) + let hostCacheEntry = hostReferences[host] ?? (host.hasPrefix("www") ? hostReferences[host.droppingWwwPrefix()] : hostReferences["www.\(host)"]) switch sizeCategory { case .small: return hostCacheEntry?.smallFaviconUrl ?? hostCacheEntry?.mediumFaviconUrl diff --git a/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift b/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift index 92b140e11d..dc5e7c1b57 100644 --- a/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift +++ b/DuckDuckGo/Feedback and Breakage/View/FeedbackViewController.swift @@ -237,7 +237,7 @@ final class FeedbackViewController: NSViewController { switch selectedFormOption { case .feedback: - if !browserFeedbackTextView.string.trimmingWhitespaces().isEmpty { + if !browserFeedbackTextView.string.trimmingWhitespace().isEmpty { submitButton.isEnabled = true } else { submitButton.isEnabled = false diff --git a/DuckDuckGo/File Download/Model/FileDownloadManager.swift b/DuckDuckGo/File Download/Model/FileDownloadManager.swift index 316823b760..7b97ee10e0 100644 --- a/DuckDuckGo/File Download/Model/FileDownloadManager.swift +++ b/DuckDuckGo/File Download/Model/FileDownloadManager.swift @@ -178,7 +178,7 @@ extension FileDownloadManager: WebKitDownloadTaskDelegate { // drop known extension, it would be appended by SavePanel var suggestedFilename = suggestedFilename if let ext = fileType?.fileExtension { - suggestedFilename = suggestedFilename.drop(suffix: "." + ext) + suggestedFilename = suggestedFilename.dropping(suffix: "." + ext) } locationChooser(suggestedFilename, downloadLocation, fileType.map { [$0] } ?? []) { url, fileType in diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift index 845f626f39..8913b3761b 100644 --- a/DuckDuckGo/Fire/Model/Fire.swift +++ b/DuckDuckGo/Fire/Model/Fire.swift @@ -78,7 +78,7 @@ final class Fire { // Drop www prefixes to produce list of burning domains static func getBurningDomain(from url: URL) -> String? { - return url.host?.dropWWW() + return url.host?.droppingWwwPrefix() } private typealias TabCollectionsCleanupInfo = [TabCollectionViewModel: [TabCollectionViewModel.TabCleanupInfo]] diff --git a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift index 4838cd126d..85e088b31b 100644 --- a/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift +++ b/DuckDuckGo/Fireproofing/Extensions/FireproofingURLExtensions.swift @@ -17,6 +17,7 @@ // import Foundation +import BrowserServicesKit private typealias URLPatterns = [String: [NSRegularExpression]] @@ -82,7 +83,7 @@ extension URL { } private func matches(any patterns: URLPatterns) -> Bool { - guard let host = self.host?.dropWWW(), + guard let host = self.host?.droppingWwwPrefix(), let matchingKey = patterns.keys.first(where: { host.contains($0) }), let pattern = patterns[matchingKey] else { return false } diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift index f7183a37c6..799124c9ca 100644 --- a/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomains.swift @@ -51,7 +51,7 @@ internal class FireproofDomains { private func loadFireproofDomains() -> FireproofDomainsContainer { dispatchPrecondition(condition: .onQueue(.main)) do { - if let domains = legacyUserDefaultsFireproofDomains?.map({ $0.dropWWW() }), + if let domains = legacyUserDefaultsFireproofDomains?.map({ $0.droppingWwwPrefix() }), !domains.isEmpty { var container = FireproofDomainsContainer() @@ -91,7 +91,7 @@ internal class FireproofDomains { return } - let domainWithoutWWW = domain.dropWWW() + let domainWithoutWWW = domain.droppingWwwPrefix() do { let id = try store.add(domainWithoutWWW) try container.add(domain: domainWithoutWWW, withId: id) @@ -129,7 +129,7 @@ internal class FireproofDomains { } func isFireproof(cookieDomain: String) -> Bool { - let domainWithoutDotPrefix = cookieDomain.drop(prefix: ".") + let domainWithoutDotPrefix = cookieDomain.dropping(prefix: ".") return container.contains(domain: domainWithoutDotPrefix, includingSuperdomains: false) || (cookieDomain.hasPrefix(".") && container.contains(superdomain: domainWithoutDotPrefix)) } diff --git a/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift b/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift index d9ca287dd9..94f527f2f4 100644 --- a/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift +++ b/DuckDuckGo/Fireproofing/Model/FireproofDomainsContainer.swift @@ -39,7 +39,7 @@ struct FireproofDomainsContainer { @discardableResult mutating func add(domain: String, withId id: NSManagedObjectID) throws -> String { - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() try domainsToIds.updateInPlace(key: domain) { value in guard value == nil else { throw DomainAlreadyAdded() } value = id @@ -57,7 +57,7 @@ struct FireproofDomainsContainer { } mutating func remove(domain: String) -> NSManagedObjectID? { - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() guard let idx = domainsToIds.index(forKey: domain) else { assertionFailure("\(domain) is not Fireproof") return nil @@ -82,7 +82,7 @@ struct FireproofDomainsContainer { } func contains(domain: String, includingSuperdomains: Bool = true) -> Bool { - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() return domainsToIds[domain] != nil || (includingSuperdomains && contains(superdomain: domain)) } diff --git a/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift b/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift index 655741fe5d..d489d8fbe6 100644 --- a/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift +++ b/DuckDuckGo/Fireproofing/View/FireproofInfoViewController.swift @@ -29,7 +29,7 @@ final class FireproofInfoViewController: NSViewController { let storyboard = NSStoryboard(name: Constants.storyboardName, bundle: nil) return storyboard.instantiateController(identifier: Constants.identifier) { coder in - return FireproofInfoViewController(coder: coder, domain: domain.dropWWW()) + return FireproofInfoViewController(coder: coder, domain: domain.droppingWwwPrefix()) } } diff --git a/DuckDuckGo/History/Model/HistoryEntry.swift b/DuckDuckGo/History/Model/HistoryEntry.swift index 7efdc8ed7c..ded1f3f198 100644 --- a/DuckDuckGo/History/Model/HistoryEntry.swift +++ b/DuckDuckGo/History/Model/HistoryEntry.swift @@ -80,7 +80,7 @@ final class HistoryEntry { func addBlockedTracker(entityName: String) { numberOfTrackersBlocked += 1 - guard !entityName.trimWhitespace().isEmpty else { + guard !entityName.trimmingWhitespace().isEmpty else { return } blockedTrackingEntities.insert(entityName) diff --git a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift index 9ba31b777b..4606bd7f09 100644 --- a/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageRecentlyVisitedModel.swift @@ -64,7 +64,7 @@ final class RecentlyVisitedModel: ObservableObject { .forEach { numberOfTrackersBlocked += $0.numberOfTrackersBlocked - guard let host = $0.url.host?.dropWWW() else { return } + guard let host = $0.url.host?.droppingWwwPrefix() else { return } var site = sitesByDomain[host] if site == nil { @@ -100,7 +100,7 @@ final class RecentlyVisitedModel: ObservableObject { bookmarkManager.update(bookmark: bookmark) site.isFavorite = bookmark.isFavorite } else { - bookmarkManager.makeBookmark(for: url, title: site.domain.dropWWW(), isFavorite: true) + bookmarkManager.makeBookmark(for: url, title: site.domain.droppingWwwPrefix(), isFavorite: true) site.isFavorite = true } } @@ -206,9 +206,9 @@ final class RecentlyVisitedSiteModel: ObservableObject { } else if !showTitlesForPagesSetting { $0.displayTitle = $0.url.absoluteString - .drop(prefix: "https://") - .drop(prefix: "http://") - .drop(prefix: $0.url.host ?? "") + .dropping(prefix: "https://") + .dropping(prefix: "http://") + .dropping(prefix: $0.url.host ?? "") } else if $0.actualTitle?.isEmpty ?? true { // Blank titles diff --git a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift index 5fadec43b2..9f2b945963 100644 --- a/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift +++ b/DuckDuckGo/Navigation Bar/View/AddressBarTextField.swift @@ -531,7 +531,7 @@ final class AddressBarTextField: NSTextField { private var stringValueWithoutSuffix: String { if let suffix = suffix { - return stringValue.drop(suffix: suffix.string) + return stringValue.dropping(suffix: suffix.string) } else { return stringValue } @@ -670,7 +670,7 @@ final class AddressBarTextField: NSTextField { @objc private func pasteAndGo(_ menuItem: NSMenuItem) { guard let pasteboardString = NSPasteboard.general.string(forType: .string), - let url = URL(trimmedAddressBarString: pasteboardString.trimmingWhitespaces()) else { + let url = URL(trimmedAddressBarString: pasteboardString.trimmingWhitespace()) else { assertionFailure("Pasteboard doesn't contain URL") return } @@ -977,7 +977,7 @@ extension AddressBarTextField: NSTextViewDelegate { } private func makePasteAndDoMenuItem() -> NSMenuItem? { - if let trimmedPasteboardString = NSPasteboard.general.string(forType: .string)?.trimmingWhitespaces(), + if let trimmedPasteboardString = NSPasteboard.general.string(forType: .string)?.trimmingWhitespace(), trimmedPasteboardString.count > 0 { if URL(trimmedAddressBarString: trimmedPasteboardString) != nil { return Self.pasteAndGoMenuItem diff --git a/DuckDuckGo/Permissions/Model/PermissionManager.swift b/DuckDuckGo/Permissions/Model/PermissionManager.swift index d215e7a0cf..c93cb211dc 100644 --- a/DuckDuckGo/Permissions/Model/PermissionManager.swift +++ b/DuckDuckGo/Permissions/Model/PermissionManager.swift @@ -52,7 +52,7 @@ final class PermissionManager: PermissionManagerProtocol { do { let entities = try store.loadPermissions() for entity in entities { - self.set(entity.permission, forDomain: entity.domain.dropWWW(), permissionType: entity.type) + self.set(entity.permission, forDomain: entity.domain.droppingWwwPrefix(), permissionType: entity.type) } } catch { os_log("PermissionStore: Failed to load permissions", type: .error) @@ -67,11 +67,11 @@ final class PermissionManager: PermissionManagerProtocol { private(set) var persistedPermissionTypes = Set() func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision { - return permissions[domain.dropWWW()]?[permissionType]?.decision ?? .ask + return permissions[domain.droppingWwwPrefix()]?[permissionType]?.decision ?? .ask } func hasPermissionPersisted(forDomain domain: String, permissionType: PermissionType) -> Bool { - return permissions[domain.dropWWW()]?[permissionType] != nil + return permissions[domain.droppingWwwPrefix()]?[permissionType] != nil } func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) { @@ -79,7 +79,7 @@ final class PermissionManager: PermissionManagerProtocol { assert(permissionType.canPersistDeniedDecision || decision != .deny) let storedPermission: StoredPermission - let domain = domain.dropWWW() + let domain = domain.droppingWwwPrefix() guard self.permission(forDomain: domain, permissionType: permissionType) != decision else { return } defer { diff --git a/DuckDuckGo/Permissions/Model/PermissionModel.swift b/DuckDuckGo/Permissions/Model/PermissionModel.swift index 358bea7e6f..4760dd654a 100644 --- a/DuckDuckGo/Permissions/Model/PermissionModel.swift +++ b/DuckDuckGo/Permissions/Model/PermissionModel.swift @@ -198,7 +198,7 @@ final class PermissionModel { to decision: PersistedPermissionDecision) { // If Always Allow/Deny for the current host: Grant/Revoke the permission - guard webView?.url?.host?.dropWWW() == domain else { return } + guard webView?.url?.host?.droppingWwwPrefix() == domain else { return } switch (decision, self.permissions[permissionType]) { case (.deny, .some): diff --git a/DuckDuckGo/Permissions/Model/PermissionType.swift b/DuckDuckGo/Permissions/Model/PermissionType.swift index 39b2a69fdd..8f4c2f7ab0 100644 --- a/DuckDuckGo/Permissions/Model/PermissionType.swift +++ b/DuckDuckGo/Permissions/Model/PermissionType.swift @@ -52,7 +52,7 @@ enum PermissionType: Hashable { case Constants.popups.rawValue: self = .popups default: if rawValue.hasPrefix(Constants.external.rawValue) { - let scheme = rawValue.drop(prefix: Constants.external.rawValue) + let scheme = rawValue.dropping(prefix: Constants.external.rawValue) guard !scheme.isEmpty else { return nil } self = .externalScheme(scheme: scheme) return diff --git a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift index 4147f58fb6..c924d827a3 100644 --- a/DuckDuckGo/Permissions/View/PermissionContextMenu.swift +++ b/DuckDuckGo/Permissions/View/PermissionContextMenu.swift @@ -42,7 +42,7 @@ final class PermissionContextMenu: NSMenu { init(permissions: [(key: PermissionType, value: PermissionState)], domain: String, delegate: PermissionContextMenuDelegate?) { - self.domain = domain.dropWWW() + self.domain = domain.droppingWwwPrefix() self.permissions = permissions self.actionDelegate = delegate super.init(title: "") diff --git a/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift b/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift index 230101ee0a..70793782e0 100644 --- a/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift +++ b/DuckDuckGo/Pinned Tabs/Model/PinnedTabsManager.swift @@ -67,7 +67,7 @@ final class PinnedTabsManager { } var pinnedDomains: Set { - Set(tabCollection.tabs.compactMap { $0.url?.host?.dropWWW() }) + Set(tabCollection.tabs.compactMap { $0.url?.host?.droppingWwwPrefix() }) } func setUp(with collection: TabCollection) { diff --git a/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift b/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift index 988754b54b..6abe1aa858 100644 --- a/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift +++ b/DuckDuckGo/Pinned Tabs/View/PinnedTabView.swift @@ -121,10 +121,10 @@ struct PinnedTabInnerView: View { if let favicon = model.favicon { Image(nsImage: favicon) .resizable() - } else if let domain = model.content.url?.host, let firstLetter = domain.dropWWW().capitalized.first.flatMap(String.init) { + } else if let domain = model.content.url?.host, let firstLetter = domain.droppingWwwPrefix().capitalized.first.flatMap(String.init) { ZStack { Rectangle() - .foregroundColor(.forDomain(domain.dropWWW())) + .foregroundColor(.forDomain(domain.droppingWwwPrefix())) Text(firstLetter) .font(.caption) .foregroundColor(.white) diff --git a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift index 92f87813b5..5e59df69e3 100644 --- a/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift +++ b/DuckDuckGo/Preferences/Model/DefaultBrowserPreferences.swift @@ -48,7 +48,7 @@ struct SystemDefaultBrowserProvider: DefaultBrowserProvider { init(bundleIdentifier: String = AppVersion.shared.identifier) { var bundleID = bundleIdentifier #if DEBUG - bundleID = bundleID.drop(suffix: ".debug") + bundleID = bundleID.dropping(suffix: ".debug") #endif self.bundleIdentifier = bundleIdentifier } diff --git a/DuckDuckGo/Preferences/Model/PreferencesSection.swift b/DuckDuckGo/Preferences/Model/PreferencesSection.swift index daeb7d89aa..b8c9197a53 100644 --- a/DuckDuckGo/Preferences/Model/PreferencesSection.swift +++ b/DuckDuckGo/Preferences/Model/PreferencesSection.swift @@ -54,7 +54,7 @@ enum PreferencePaneIdentifier: String, Equatable, Hashable, Identifiable { init?(url: URL) { // manually extract path because URLs such as "about:preferences" can't figure out their host or path - let path = url.absoluteString.drop(prefix: URL.preferences.absoluteString + "/") + let path = url.absoluteString.dropping(prefix: URL.preferences.absoluteString + "/") self.init(rawValue: path) } diff --git a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift index afa1797804..76b88b885e 100644 --- a/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift +++ b/DuckDuckGo/Preferences/View/FireproofDomainsViewController.swift @@ -98,7 +98,7 @@ extension FireproofDomainsViewController: NSTableViewDataSource, NSTableViewDele func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if let cell = tableView.makeView(withIdentifier: Constants.cellIdentifier, owner: nil) as? NSTableCellView { let domain = fireproofDomains[row] - cell.textField?.stringValue = domain.dropWWW() + cell.textField?.stringValue = domain.droppingWwwPrefix() cell.imageView?.image = faviconManagement.getCachedFavicon(for: domain, sizeCategory: .small)?.image cell.imageView?.applyFaviconStyle() @@ -122,7 +122,7 @@ extension FireproofDomainsViewController: NSTextFieldDelegate { if field.stringValue.isEmpty { filteredFireproofDomains = nil } else { - filteredFireproofDomains = allFireproofDomains.filter { $0.dropWWW().contains(field.stringValue) } + filteredFireproofDomains = allFireproofDomains.filter { $0.droppingWwwPrefix().contains(field.stringValue) } } reloadData() diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift index 040dfda212..0ec658e6e6 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementItemListModel.swift @@ -112,7 +112,7 @@ enum SecureVaultItem: Equatable, Identifiable, Comparable { var displayTitle: String { switch self { case .account(let account): - return ((account.title ?? "").isEmpty == true ? account.domain.dropWWW() : account.title) ?? "" + return ((account.title ?? "").isEmpty == true ? account.domain.droppingWwwPrefix() : account.title) ?? "" case .card(let card): return card.title case .identity(let identity): diff --git a/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift b/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift index afa6b83b4c..5b075ffab7 100644 --- a/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift +++ b/DuckDuckGo/Secure Vault/Model/PasswordManagementLoginModel.swift @@ -98,13 +98,13 @@ final class PasswordManagementLoginModel: ObservableObject, PasswordManagementIt } func normalizedDomain(_ domain: String) -> String { - let trimmed = domain.trimmingWhitespaces() + let trimmed = domain.trimmingWhitespace() if !trimmed.starts(with: "https://") && !trimmed.starts(with: "http://") && trimmed.contains("://") { // Contains some other protocol, so don't mess with it return domain } - let noSchemeOrWWW = domain.drop(prefix: "https://").drop(prefix: "http://").dropWWW() + let noSchemeOrWWW = domain.dropping(prefix: "https://").dropping(prefix: "http://").droppingWwwPrefix() return URLComponents(string: "https://\(noSchemeOrWWW)")?.host ?? "" } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift index 91c9ae291d..50763b3423 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementLoginItemView.swift @@ -332,12 +332,12 @@ private struct HeaderView: View { if model.isNew || model.isEditing { - TextField(model.domain.dropWWW(), text: $model.title) + TextField(model.domain.droppingWwwPrefix(), text: $model.title) .font(.title) } else { - Text(model.title.isEmpty ? model.domain.dropWWW() : model.title) + Text(model.title.isEmpty ? model.domain.droppingWwwPrefix() : model.title) .font(.title) } diff --git a/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift b/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift index 726b8cffb0..5f150d56cd 100644 --- a/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift +++ b/DuckDuckGo/Secure Vault/View/PasswordManagementViewController.swift @@ -208,7 +208,7 @@ final class PasswordManagementViewController: NSViewController { // Only select the matching item directly if macOS 11 is available, as 10.15 doesn't support scrolling directly to a given // item in SwiftUI. On 10.15, show the matching item by filtering the search bar automatically instead. if #available(macOS 11.0, *) { - refetchWithText("", selectItemMatchingDomain: domain?.dropWWW(), clearWhenNoMatches: true) + refetchWithText("", selectItemMatchingDomain: domain?.droppingWwwPrefix(), clearWhenNoMatches: true) } else { refetchWithText(isDirty ? "" : domain ?? "", clearWhenNoMatches: true) } @@ -692,7 +692,7 @@ final class PasswordManagementViewController: NSViewController { } private func updateFilter() { - let text = searchField.stringValue.trimmingWhitespaces() + let text = searchField.stringValue.trimmingWhitespace() listModel?.filter = text } diff --git a/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift b/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift index c143345389..6b0c71d5e0 100644 --- a/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift +++ b/DuckDuckGo/Secure Vault/View/SaveCredentialsViewController.swift @@ -120,7 +120,7 @@ final class SaveCredentialsViewController: NSViewController { self.delegate?.shouldCloseSaveCredentialsViewController(self) } - var account = SecureVaultModels.WebsiteAccount(username: usernameField.stringValue.trimmingWhitespaces(), + var account = SecureVaultModels.WebsiteAccount(username: usernameField.stringValue.trimmingWhitespace(), domain: domainLabel.stringValue) account.id = credentials?.account.id let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) @@ -169,7 +169,7 @@ final class SaveCredentialsViewController: NSViewController { return } - let alert = NSAlert.fireproofAlert(with: host.dropWWW()) + let alert = NSAlert.fireproofAlert(with: host.droppingWwwPrefix()) alert.beginSheetModal(for: window) { response in if response == NSApplication.ModalResponse.alertFirstButtonReturn { Pixel.fire(.fireproof(kind: .pwm, suggested: .suggested)) diff --git a/DuckDuckGo/User Agent/Model/UserAgent.swift b/DuckDuckGo/User Agent/Model/UserAgent.swift index 9ff4234d80..06373b0951 100644 --- a/DuckDuckGo/User Agent/Model/UserAgent.swift +++ b/DuckDuckGo/User Agent/Model/UserAgent.swift @@ -17,6 +17,7 @@ // import Foundation +import BrowserServicesKit enum UserAgent { diff --git a/Unit Tests/Common/Extensions/URLExtensionTests.swift b/Unit Tests/Common/Extensions/URLExtensionTests.swift index a1080b241b..c503a01320 100644 --- a/Unit Tests/Common/Extensions/URLExtensionTests.swift +++ b/Unit Tests/Common/Extensions/URLExtensionTests.swift @@ -22,26 +22,6 @@ import Combine final class URLExtensionTests: XCTestCase { - func test_external_urls_are_valid() { - XCTAssertTrue("mailto://user@host.tld".url!.isValid) - XCTAssertTrue("sms://+44776424232323".url!.isValid) - XCTAssertTrue("ftp://example.com".url!.isValid) - } - - func test_navigational_urls_are_valid() { - XCTAssertTrue("http://example.com".url!.isValid) - XCTAssertTrue("https://example.com".url!.isValid) - XCTAssertTrue("http://localhost".url!.isValid) - XCTAssertTrue("http://localdomain".url!.isValid) - } - - func test_when_no_scheme_in_string_url_has_scheme() { - XCTAssertEqual("duckduckgo.com".url!.absoluteString, "http://duckduckgo.com") - XCTAssertEqual("example.com".url!.absoluteString, "http://example.com") - XCTAssertEqual("localhost".url!.absoluteString, "http://localhost") - XCTAssertNil("localdomain".url) - } - func test_makeURL_from_addressBarString() { let data: [(string: String, expected: String)] = [ ("https://duckduckgo.com/?q=search string with spaces", "https://duckduckgo.com/?q=search%20string%20with%20spaces"), @@ -103,81 +83,4 @@ final class URLExtensionTests: XCTestCase { } } - func testWhenPunycodeUrlIsCalledOnEmptyStringThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: "")?.absoluteString) - } - - func testWhenPunycodeUrlIsCalledOnQueryThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: " ")?.absoluteString) - } - - func testWhenPunycodeUrlIsCalledOnQueryWithSpaceThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: "https://www.duckduckgo .com/html?q=search")?.absoluteString) - XCTAssertNil(URL(trimmedAddressBarString: "https://www.duckduckgo.com/html?q =search")?.absoluteString) - } - - func testWhenPunycodeUrlIsCalledOnLocalHostnameThenUrlIsNotReturned() { - XCTAssertNil(URL(trimmedAddressBarString: "πŸ’©")?.absoluteString) - } - - func testWhenDefineSearchRequestIsMadeItIsNotInterpretedAsLocalURL() { - XCTAssertNil(URL(trimmedAddressBarString: "define:300/spartans")?.absoluteString) - } - - func testAddressBarURLParsing() { - let addresses = [ - "user@somehost.local:9091/index.html", - "something.local:9100", - "user@localhost:5000", - "user:password@localhost:5000", - "localhost", - "localhost:5000", - "sms://+44123123123", - "mailto:test@example.com", - "https://", - "http://duckduckgo.com", - "https://duckduckgo.com", - "https://duckduckgo.com/", - "duckduckgo.com", - "duckduckgo.com/html?q=search", - "www.duckduckgo.com", - "https://www.duckduckgo.com/html?q=search", - "https://www.duckduckgo.com/html/?q=search", - "ftp://www.duckduckgo.com", - "file:///users/user/Documents/afile" - ] - - for address in addresses { - let url = URL(trimmedAddressBarString: address) - var expectedString = address - let expectedScheme = address.split(separator: "/").first.flatMap { - $0.hasSuffix(":") ? String($0).drop(suffix: ":") : nil - }?.lowercased() ?? "http" - if !address.hasPrefix(expectedScheme) { - expectedString = expectedScheme + "://" + address - } - XCTAssertEqual(url?.scheme, expectedScheme) - XCTAssertEqual(url?.absoluteString, expectedString) - } - } - - func testWhenPunycodeUrlIsCalledWithEncodedUrlsThenUrlIsReturned() { - XCTAssertEqual("http://xn--ls8h.la", "πŸ’©.la".decodedURL?.absoluteString) - XCTAssertEqual("http://xn--ls8h.la/", "πŸ’©.la/".decodedURL?.absoluteString) - XCTAssertEqual("http://82.xn--b1aew.xn--p1ai", "82.ΠΌΠ²Π΄.Ρ€Ρ„".decodedURL?.absoluteString) - XCTAssertEqual("http://xn--ls8h.la:8080", "http://πŸ’©.la:8080".decodedURL?.absoluteString) - XCTAssertEqual("http://xn--ls8h.la", "http://πŸ’©.la".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la", "https://πŸ’©.la".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/", "https://πŸ’©.la/".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/path/to/resource", "https://πŸ’©.la/path/to/resource".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/path/to/resource?query=true", "https://πŸ’©.la/path/to/resource?query=true".decodedURL?.absoluteString) - XCTAssertEqual("https://xn--ls8h.la/%F0%9F%92%A9", "https://πŸ’©.la/πŸ’©".decodedURL?.absoluteString) - } - -} - -private extension String { - var decodedURL: URL? { - URL(trimmedAddressBarString: self) - } } diff --git a/Unit Tests/Permissions/PermissionManagerMock.swift b/Unit Tests/Permissions/PermissionManagerMock.swift index ae7be64654..22f273d92d 100644 --- a/Unit Tests/Permissions/PermissionManagerMock.swift +++ b/Unit Tests/Permissions/PermissionManagerMock.swift @@ -30,16 +30,16 @@ final class PermissionManagerMock: PermissionManagerProtocol { var savedPermissions = [String: [PermissionType: Bool]]() func permission(forDomain domain: String, permissionType: PermissionType) -> PersistedPermissionDecision { - guard let allow = savedPermissions[domain.dropWWW()]?[permissionType] else { return .ask } + guard let allow = savedPermissions[domain.droppingWwwPrefix()]?[permissionType] else { return .ask } return PersistedPermissionDecision(allow: allow, isRemoved: false) } func setPermission(_ decision: PersistedPermissionDecision, forDomain domain: String, permissionType: PermissionType) { - savedPermissions[domain.dropWWW(), default: [:]][permissionType] = decision == .ask ? nil : decision.boolValue + savedPermissions[domain.droppingWwwPrefix(), default: [:]][permissionType] = decision == .ask ? nil : decision.boolValue } func removePermission(forDomain domain: String, permissionType: PermissionType) { - savedPermissions[domain.dropWWW(), default: [:]][permissionType] = nil + savedPermissions[domain.droppingWwwPrefix(), default: [:]][permissionType] = nil } var burnPermissionsCalled = false diff --git a/Unit Tests/Permissions/PermissionManagerTests.swift b/Unit Tests/Permissions/PermissionManagerTests.swift index e48d68dd39..da1f751a90 100644 --- a/Unit Tests/Permissions/PermissionManagerTests.swift +++ b/Unit Tests/Permissions/PermissionManagerTests.swift @@ -34,7 +34,7 @@ final class PermissionManagerTests: XCTestCase { store.permissions = [.entity1, .entity2] let result1 = manager.permission(forDomain: "www." + PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type) - let result2 = manager.permission(forDomain: PermissionEntity.entity2.domain.dropWWW(), + let result2 = manager.permission(forDomain: PermissionEntity.entity2.domain.droppingWwwPrefix(), permissionType: PermissionEntity.entity2.type) let result3 = manager.permission(forDomain: "otherdomain.com", permissionType: .microphone) @@ -115,7 +115,7 @@ final class PermissionManagerTests: XCTestCase { .add(domain: PermissionEntity.entity1.domain, permissionType: PermissionEntity.entity1.type, decision: .allow), - .add(domain: PermissionEntity.entity2.domain.dropWWW(), + .add(domain: PermissionEntity.entity2.domain.droppingWwwPrefix(), permissionType: PermissionEntity.entity2.type, decision: .deny)]) XCTAssertEqual(result1, .allow) @@ -182,7 +182,7 @@ final class PermissionManagerTests: XCTestCase { let e = expectation(description: "permission published") let c = manager.permissionPublisher.sink { value in - XCTAssertEqual(value.domain, PermissionEntity.entity2.domain.dropWWW()) + XCTAssertEqual(value.domain, PermissionEntity.entity2.domain.droppingWwwPrefix()) XCTAssertEqual(value.permissionType, PermissionEntity.entity2.type) XCTAssertEqual(value.decision, .ask) e.fulfill() @@ -218,7 +218,7 @@ final class PermissionManagerTests: XCTestCase { let fireproofDomains = FireproofDomains(store: FireproofDomainsStoreMock()) fireproofDomains.add(domain: PermissionEntity.entity1.domain) - manager.burnPermissions(of: [PermissionEntity.entity2.domain.dropWWW()]) {} + manager.burnPermissions(of: [PermissionEntity.entity2.domain.droppingWwwPrefix()]) {} XCTAssertEqual(store.history, [.load, .clear(exceptions: [PermissionEntity.entity1.permission])]) XCTAssertEqual(manager.permission(forDomain: PermissionEntity.entity1.domain, From 116f7811ea7887a3083b4869d9f26bfa35eb5ba8 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 26 Aug 2022 03:11:52 -0700 Subject: [PATCH 06/11] Use LazyVStack to improve new tab page performance. (#699) This PR updates the new tab page to use a LazyVStack on macOS 11 and up. This dramatically increases performance for users who have 1000+ bookmarks. To make this easier, I pulled a couple small custom buttons out into their own views, and then did the same with the main VStack code. --- DuckDuckGo/Home Page/View/FavoritesView.swift | 117 ++++++++++++------ 1 file changed, 81 insertions(+), 36 deletions(-) diff --git a/DuckDuckGo/Home Page/View/FavoritesView.swift b/DuckDuckGo/Home Page/View/FavoritesView.swift index 8754975d66..f79a869a94 100644 --- a/DuckDuckGo/Home Page/View/FavoritesView.swift +++ b/DuckDuckGo/Home Page/View/FavoritesView.swift @@ -32,58 +32,103 @@ struct Favorites: View { var body: some View { - let addButton = ZStack(alignment: .top) { - FavoriteTemplate(title: UserText.addFavorite, domain: nil) - ZStack { - Image("Add") - .resizable() - .frame(width: 22, height: 22) - }.frame(width: 64, height: 64) - } - .link { - model.addNew() + if #available(macOS 11.0, *) { + LazyVStack(spacing: 4) { + FavoritesGrid(isHovering: $isHovering) + } + .frame(maxWidth: .infinity) + .onHover { isHovering in + self.isHovering = isHovering + } + } else { + VStack(spacing: 4) { + FavoritesGrid(isHovering: $isHovering) + } + .frame(maxWidth: .infinity) + .onHover { isHovering in + self.isHovering = isHovering + } } - let ghostButton = VStack { - RoundedRectangle(cornerRadius: 12) - .stroke(Color("HomeFavoritesGhostColor"), style: StrokeStyle(lineWidth: 1.5, dash: [4.0, 2.0])) - .frame(width: 64, height: 64) - }.frame(width: 64) + } - VStack(spacing: 4) { +} + +struct FavoritesGrid: View { + + @EnvironmentObject var model: HomePage.Models.FavoritesModel + + @Binding var isHovering: Bool + + var rowIndices: Range { + model.showAllFavorites ? model.rows.indices : model.rows.indices.prefix(HomePage.favoritesRowCountWhenCollapsed) + } + + var body: some View { - ForEach(rowIndices, id: \.self) { index in + ForEach(rowIndices, id: \.self) { index in - HStack(alignment: .top, spacing: 20) { - ForEach(model.rows[index], id: \.id) { favorite in + HStack(alignment: .top, spacing: 20) { + ForEach(model.rows[index], id: \.id) { favorite in - switch favorite.favoriteType { - case .bookmark(let bookmark): - Favorite(bookmark: bookmark) + switch favorite.favoriteType { + case .bookmark(let bookmark): + Favorite(bookmark: bookmark) - case .addButton: - addButton + case .addButton: + FavoritesGridAddButton() - case .ghostButton: - ghostButton - } + case .ghostButton: + FavoritesGridGhostButton() } } - } + + } - MoreOrLess(isExpanded: $model.showAllFavorites) - .padding(.top, 2) - .visibility(model.rows.count > HomePage.favoritesRowCountWhenCollapsed && isHovering ? .visible : .invisible) + MoreOrLess(isExpanded: $model.showAllFavorites) + .padding(.top, 2) + .visibility(model.rows.count > HomePage.favoritesRowCountWhenCollapsed && isHovering ? .visible : .invisible) + } + +} + +private struct FavoritesGridAddButton: View { + + @EnvironmentObject var model: HomePage.Models.FavoritesModel + + var body: some View { + + ZStack(alignment: .top) { + FavoriteTemplate(title: UserText.addFavorite, domain: nil) + ZStack { + Image("Add") + .resizable() + .frame(width: 22, height: 22) + }.frame(width: 64, height: 64) } - .frame(maxWidth: .infinity) - .onHover { isHovering in - self.isHovering = isHovering + .link { + model.addNew() } - + } - + +} + +private struct FavoritesGridGhostButton: View { + + var body: some View { + + VStack { + RoundedRectangle(cornerRadius: 12) + .stroke(Color("HomeFavoritesGhostColor"), style: StrokeStyle(lineWidth: 1.5, dash: [4.0, 2.0])) + .frame(width: 64, height: 64) + } + .frame(width: 64) + + } + } struct FavoriteTemplate: View { From 1d89288bb518972b6a223671e6e4bd4d583e01fe Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 26 Aug 2022 12:46:40 +0100 Subject: [PATCH 07/11] Possible fix for errant pop-up (#694) Task/Issue URL: https://app.asana.com/0/1199230911884351/1202831359420937/f Description: Possible fix for errant pop-up. The main goal with this change is to release the pop-up once the user picks an option and to make sure the pop-up tab and the current tab are not nil before showing it --- .../Autoconsent/UI/CookieConsentPopover.swift | 3 +- .../UI/CookieConsentPopoverManager.swift | 48 +++++++++++++++---- .../View/BrowserTabViewController.swift | 14 +++--- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift index 118d376dc4..272bc41115 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentPopover.swift @@ -48,7 +48,7 @@ public final class CookieConsentPopover { viewController.delegate = self } - public func close(animated: Bool) { + public func close(animated: Bool, completion: (() -> Void)? = nil) { guard let overlayWindow = windowController.window else { return } @@ -57,6 +57,7 @@ public final class CookieConsentPopover { let removeWindow = { overlayWindow.parent?.removeChildWindow(overlayWindow) overlayWindow.orderOut(nil) + completion?() } if animated { diff --git a/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift b/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift index 62f3466b79..299139e64f 100644 --- a/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift +++ b/DuckDuckGo/Autoconsent/UI/CookieConsentPopoverManager.swift @@ -22,21 +22,51 @@ final class CookieConsentPopoverManager: CookieConsentPopoverDelegate { var completion: ((Bool) -> Void)? weak var currentTab: Tab? - lazy var popOver: CookieConsentPopover = { - let popover = CookieConsentPopover() - popover.delegate = self - return popover - }() + private(set) var popOver: CookieConsentPopover? func cookieConsentPopover(_ popOver: CookieConsentPopover, didFinishWithResult result: Bool) { - popOver.close(animated: true) + popOver.close(animated: true) { [weak self] in + self?.popOver = nil + self?.currentTab = nil + } + if let completion = completion { completion(result) } } + + func show(on view: NSView, animated: Bool, result: ((Bool) -> Void)? = nil) { + preparePopover() + + guard let popOver = popOver else { + return + } - func show(on view: NSView, animated: Bool, result: @escaping (Bool) -> Void) { - popOver.show(on: view, animated: true) - self.completion = result + popOver.show(on: view, animated: animated) + if let result = result { + self.completion = result + } + } + + func close(animated: Bool) { + guard let popOver = popOver else { + return + } + + popOver.close(animated: animated) + } + + private func preparePopover() { + // If the tab was closed, we want to start the animation again + if currentTab == nil { + popOver = nil + } + + guard popOver == nil else { + return + } + + popOver = CookieConsentPopover() + popOver?.delegate = self } } diff --git a/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift b/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift index 8350040eca..9b8a7692d2 100644 --- a/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift +++ b/DuckDuckGo/Browser Tab/View/BrowserTabViewController.swift @@ -114,7 +114,7 @@ final class BrowserTabViewController: NSViewController { private func windowWillClose(_ notification: NSNotification) { self.removeWebViewFromHierarchy() } - + private func subscribeToSelectedTabViewModel() { tabCollectionViewModel.$selectedTabViewModel .sink { [weak self] selectedTabViewModel in @@ -128,12 +128,14 @@ final class BrowserTabViewController: NSViewController { } .store(in: &cancellables) } - + private func showCookieConsentPopoverIfNecessary(_ selectedTabViewModel: TabViewModel?) { - if selectedTabViewModel?.tab == cookieConsentPopoverManager.currentTab { - cookieConsentPopoverManager.popOver.show(on: view, animated: false) + if let selectedTab = selectedTabViewModel?.tab, + let cookiePopoverTab = cookieConsentPopoverManager.currentTab, + selectedTab == cookiePopoverTab { + cookieConsentPopoverManager.show(on: view, animated: false) } else { - cookieConsentPopoverManager.popOver.close(animated: false) + cookieConsentPopoverManager.close(animated: false) } } @@ -475,7 +477,7 @@ extension BrowserTabViewController: TabDelegate { func tab(_ tab: Tab, promptUserForCookieConsent result: @escaping (Bool) -> Void) { cookieConsentPopoverManager.show(on: view, animated: true, result: result) - cookieConsentPopoverManager.currentTab = tabViewModel?.tab + cookieConsentPopoverManager.currentTab = tab } func tabWillStartNavigation(_ tab: Tab, isUserInitiated: Bool) { From 83764f5c987ed0397692326d36e6d11f68dd3b7a Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Fri, 26 Aug 2022 14:28:51 +0100 Subject: [PATCH 08/11] update embedded files --- .../AppPrivacyConfigurationDataProvider.swift | 4 ++-- DuckDuckGo/Content Blocker/macos-config.json | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift b/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift index 2d1c0f465a..c207dc7903 100644 --- a/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift +++ b/DuckDuckGo/Content Blocker/AppPrivacyConfigurationDataProvider.swift @@ -22,8 +22,8 @@ import BrowserServicesKit final class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider { public struct Constants { - public static let embeddedDataETag = "\"f7199de225a3ac90f4bca3919e87bf22\"" - public static let embeddedDataSHA = "8fead7748dda734d7340ff92cad2e00bed92197dce894905ea3ab3a94f553dc3" + public static let embeddedDataETag = "\"447be02a9a041ab4a7cf2d8d53da119f\"" + public static let embeddedDataSHA = "0089a2b38fb66a8d9529fe864c5821724fee8f1ff7ab6a38b30460fbce9f3964" } var embeddedDataEtag: String { diff --git a/DuckDuckGo/Content Blocker/macos-config.json b/DuckDuckGo/Content Blocker/macos-config.json index f0ddce78ae..aa02ce2797 100644 --- a/DuckDuckGo/Content Blocker/macos-config.json +++ b/DuckDuckGo/Content Blocker/macos-config.json @@ -1,6 +1,6 @@ { "readme": "https://github.com/duckduckgo/privacy-configuration", - "version": 1661270003722, + "version": 1661422548137, "features": { "adClickAttribution": { "readme": "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/#3rd-party-tracker-loading-protection", @@ -73,6 +73,10 @@ { "domain": "freecodecamp.org", "reason": "Clicking 'get started' reloads the page and does not progress to the login page." + }, + { + "domain": "www.audiosciencereview.com", + "reason": "Pages on the site end up in redirect loops in Firefox." } ], "settings": { From f96856c78744b634ccd10ccb000e454072981968 Mon Sep 17 00:00:00 2001 From: Brad Slayter Date: Fri, 26 Aug 2022 09:34:07 -0500 Subject: [PATCH 09/11] Update BSK (#693) * Update BSK * Pin to BSK 27.0.1 --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 08b82b7ef1..e5c5e85c8b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -6212,7 +6212,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 27.0.0; + version = 27.0.1; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { From 95be7fb5b2a94ab403a711829fb5159738d45f4e Mon Sep 17 00:00:00 2001 From: Chris Brind Date: Fri, 26 Aug 2022 16:19:45 +0100 Subject: [PATCH 10/11] update version --- DuckDuckGo.xcodeproj/project.pbxproj | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index e5c5e85c8b..5e81db4c81 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -5268,7 +5268,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "REVIEW=1"; @@ -5277,7 +5277,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.review; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "DuckDuckGo Review"; @@ -5419,7 +5419,7 @@ CODE_SIGN_IDENTITY = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5427,7 +5427,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -5619,7 +5619,7 @@ CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5627,7 +5627,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -5872,7 +5872,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5880,7 +5880,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.debug; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -5901,7 +5901,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = DuckDuckGo/Info.plist; @@ -5909,7 +5909,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = DuckDuckGo; @@ -6034,7 +6034,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 0.28.5; + CURRENT_PROJECT_VERSION = 0.28.6; DEVELOPMENT_TEAM = HKE973VLUW; ENABLE_HARDENED_RUNTIME = YES; GCC_PREPROCESSOR_DEFINITIONS = "REVIEW=1"; @@ -6043,7 +6043,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.28.5; + MARKETING_VERSION = 0.28.6; PRODUCT_BUNDLE_IDENTIFIER = com.duckduckgo.macos.browser.review; PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; PRODUCT_NAME = "DuckDuckGo Review"; From 689544792169c699aec0e7f5f09f598e90707821 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Fri, 26 Aug 2022 10:37:35 -0700 Subject: [PATCH 11/11] Force callers to refresh even when the save fails. (#701) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's possible for the bookmarks store to return an error even when the actual save call succeeded, as Core Data can return errors unrelated to the object you're saving. In this case, the caller will not refresh itself, even despite the data actually having successfully changed under the hood. This PR updates the failure completion handlers to trigger a refresh regardless. There's more work to do here to track when and how failures happen at all, but this will solve the problem that Alex was seeing. The issue that Alex is seeing is already repaired when you launch the app, but it's not repaired if an error is encountered at runtime. I'd like to fix that, but want to take more time to do it correctly, so I'll prepare another PR next week – this PR will be enough to get around his issue. --- DuckDuckGo/Bookmarks/Services/BookmarkStore.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift index e718be5718..4cb1f939f6 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStore.swift @@ -202,7 +202,7 @@ final class LocalBookmarkStore: BookmarkStore { try self.context.save() } catch { assertionFailure("LocalBookmarkStore: Saving of context failed") - DispatchQueue.main.async { completion(false, error) } + DispatchQueue.main.async { completion(true, error) } return } @@ -232,7 +232,7 @@ final class LocalBookmarkStore: BookmarkStore { try self.context.save() } catch { assertionFailure("LocalBookmarkStore: Saving of context failed") - DispatchQueue.main.async { completion(false, error) } + DispatchQueue.main.async { completion(true, error) } } DispatchQueue.main.async { completion(true, nil) } @@ -405,7 +405,7 @@ final class LocalBookmarkStore: BookmarkStore { assertionFailure("LocalBookmarkStore: Saving of context failed") } - DispatchQueue.main.async { completion(false, error) } + DispatchQueue.main.async { completion(true, error) } return }