From 804d8d98f4a878337b65883689f37debd7fa24e9 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Tue, 27 Sep 2022 09:04:23 -0700 Subject: [PATCH 01/27] Fire a pixel when WebKit terminates. (#728) Task/Issue URL: https://app.asana.com/0/1199230911884351/1203044296270964/f Tech Design URL: CC: Description: This PR adds conformance to WebKit's webViewWebContentProcessDidTerminate(_:) method and uses it to fire a pixel. --- DuckDuckGo/Browser Tab/Model/Tab.swift | 4 ++++ DuckDuckGo/Statistics/PixelEvent.swift | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index bd71c4c52e..df85bedc4e 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -1429,6 +1429,10 @@ extension Tab: WKNavigationDelegate { guard frame.isMainFrame else { return } self.mainFrameLoadState = .finished } + + func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { + Pixel.fire(.debug(event: .webKitDidTerminate)) + } } // universal download event handlers for Legacy _WKDownload and modern WKDownload diff --git a/DuckDuckGo/Statistics/PixelEvent.swift b/DuckDuckGo/Statistics/PixelEvent.swift index 88f9541286..5b497309d3 100644 --- a/DuckDuckGo/Statistics/PixelEvent.swift +++ b/DuckDuckGo/Statistics/PixelEvent.swift @@ -230,6 +230,8 @@ extension Pixel { case adAttributionLogicRequestingAttributionTimedOut case adAttributionLogicWrongVendorOnSuccessfulCompilation case adAttributionLogicWrongVendorOnFailedCompilation + + case webKitDidTerminate } } @@ -513,6 +515,9 @@ extension Pixel.Event.Debug { return "ad_attribution_logic_wrong_vendor_on_successful_compilation" case .adAttributionLogicWrongVendorOnFailedCompilation: return "ad_attribution_logic_wrong_vendor_on_failed_compilation" + + case .webKitDidTerminate: + return "webkit_did_terminate" } } } From d1a9967353c5cc442c7b632fa9db9c98131cbd6a Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 28 Sep 2022 08:16:46 -0700 Subject: [PATCH 02/27] Clean up Xcode 14 warnings and main thread checker violations (#725) Task/Issue URL: https://app.asana.com/0/1199230911884351/1203033086199466/f Tech Design URL: CC: Description: This PR fixes warnings visible in Xcode 14, and addresses some main thread checker violations that are now being caught. --- DuckDuckGo.xcodeproj/project.pbxproj | 6 +++--- DuckDuckGo/Browser Tab/Model/Tab.swift | 17 ++++++++--------- .../File Download/Model/FileDownloadError.swift | 2 +- .../Model/HomePageFavoritesModel.swift | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 7676f8763d..3518b355e9 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -373,7 +373,6 @@ 85B7184A27677C2D00B4277F /* Onboarding.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 85B7184927677C2D00B4277F /* Onboarding.storyboard */; }; 85B7184C27677C6500B4277F /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184B27677C6500B4277F /* OnboardingViewController.swift */; }; 85B7184E27677CBB00B4277F /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B7184D27677CBB00B4277F /* RootView.swift */; }; - 85B8757F28B903D900D39E04 /* Configuration.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 85B8757E28B903D900D39E04 /* Configuration.xcconfig */; }; 85C48CCC278D808F00D3263E /* NSAttributedStringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C48CCB278D808F00D3263E /* NSAttributedStringExtension.swift */; }; 85C48CD127908C1000D3263E /* BrowserImportMoreInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C48CD027908C1000D3263E /* BrowserImportMoreInfoViewController.swift */; }; 85C5991B27D10CF000E605B2 /* FireAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C5991A27D10CF000E605B2 /* FireAnimationView.swift */; }; @@ -4301,7 +4300,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 85B8757F28B903D900D39E04 /* Configuration.xcconfig in Resources */, 4B02198C25E05FAC00ED7DEA /* Fireproofing.storyboard in Resources */, AA80EC73256C46A2007083E7 /* Suggestion.storyboard in Resources */, AA693E5E2696E5B90007BB78 /* CrashReports.storyboard in Resources */, @@ -4396,6 +4394,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3705272528992C8A000C06A2 /* Check Embedded Config URLs */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -4414,6 +4413,7 @@ }; AA8EDF2824925E940071C2E8 /* Swift Lint */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); diff --git a/DuckDuckGo/Browser Tab/Model/Tab.swift b/DuckDuckGo/Browser Tab/Model/Tab.swift index df85bedc4e..da38346e73 100644 --- a/DuckDuckGo/Browser Tab/Model/Tab.swift +++ b/DuckDuckGo/Browser Tab/Model/Tab.swift @@ -524,7 +524,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { if !didRestore { if url.isFileURL { - webView.loadFileURL(url, allowingReadAccessTo: URL(fileURLWithPath: "/")) + _ = webView.loadFileURL(url, allowingReadAccessTo: URL(fileURLWithPath: "/")) } else { webView.load(url) } @@ -573,7 +573,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { var didRestore: Bool = false if let sessionStateData = self.sessionStateData { if contentURL.isFileURL { - webView.loadFileURL(contentURL, allowingReadAccessTo: URL(fileURLWithPath: "/")) + _ = webView.loadFileURL(contentURL, allowingReadAccessTo: URL(fileURLWithPath: "/")) } do { try webView.restoreSessionState(from: sessionStateData) @@ -592,7 +592,7 @@ final class Tab: NSObject, Identifiable, ObservableObject { var didRestore: Bool = false if let interactionStateData = self.interactionStateData { if contentURL.isFileURL { - webView.loadFileURL(contentURL, allowingReadAccessTo: URL(fileURLWithPath: "/")) + _ = webView.loadFileURL(contentURL, allowingReadAccessTo: URL(fileURLWithPath: "/")) } webView.interactionState = interactionStateData @@ -602,7 +602,6 @@ final class Tab: NSObject, Identifiable, ObservableObject { return didRestore } - @MainActor private func addHomePageToWebViewIfNeeded() { guard !AppDelegate.isRunningTests else { return } if content == .homePage && webView.url == nil { @@ -636,17 +635,17 @@ final class Tab: NSObject, Identifiable, ObservableObject { superviewObserver = webView.observe(\.superview, options: .old) { [weak self] _, change in // if the webView is being added to superview - reload if needed if case .some(.none) = change.oldValue { - Task { [weak self] in + Task { @MainActor [weak self] in await self?.reloadIfNeeded() } } } // background tab loading should start immediately - Task { + Task { @MainActor in await reloadIfNeeded(shouldLoadInBackground: shouldLoadInBackground) if !shouldLoadInBackground { - await addHomePageToWebViewIfNeeded() + addHomePageToWebViewIfNeeded() } } } @@ -1144,7 +1143,7 @@ extension Tab: WKNavigationDelegate { if let newRequest = referrerTrimming.trimReferrer(forNavigation: navigationAction, originUrl: webView.url ?? navigationAction.sourceFrame.webView?.url) { defer { - webView.load(newRequest) + _ = webView.load(newRequest) } return .cancel } @@ -1164,7 +1163,7 @@ extension Tab: WKNavigationDelegate { let request = GPCRequestFactory.shared.requestForGPC(basedOn: navigationAction.request) { self.invalidateBackItemIfNeeded(for: navigationAction) defer { - webView.load(request) + _ = webView.load(request) } return .cancel } diff --git a/DuckDuckGo/File Download/Model/FileDownloadError.swift b/DuckDuckGo/File Download/Model/FileDownloadError.swift index cfc82be43d..0b147aa788 100644 --- a/DuckDuckGo/File Download/Model/FileDownloadError.swift +++ b/DuckDuckGo/File Download/Model/FileDownloadError.swift @@ -93,7 +93,7 @@ extension FileDownloadError: Equatable { static func == (lhs: FileDownloadError, rhs: FileDownloadError) -> Bool { switch lhs { case .failedToMoveFileToDownloads: if case .failedToMoveFileToDownloads = rhs { return true } - case .failedToCompleteDownloadTask(underlyingError: let error1, resumeData: let data1) : + case .failedToCompleteDownloadTask(underlyingError: let error1, resumeData: let data1): if case .failedToCompleteDownloadTask(underlyingError: let error2, resumeData: let data2) = rhs { return type(of: error1) == type(of: error2) && data1?.count == data2?.count } diff --git a/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift b/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift index 5eb2789633..1468e92dc6 100644 --- a/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift +++ b/DuckDuckGo/Home Page/Model/HomePageFavoritesModel.swift @@ -78,7 +78,7 @@ extension HomePage.Models { init(open: @escaping (Bookmark, OpenTarget) -> Void, removeFavorite: @escaping (Bookmark) -> Void, deleteBookmark: @escaping (Bookmark) -> Void, - addEdit: @escaping (Bookmark?) -> Void) { + addEdit: @escaping (Bookmark?) -> Void) { self.showAllFavorites = Self.showAllFavoritesSetting self.open = open From e6ec49c03a86812ac3c231a3bdc1c35af7cf788c Mon Sep 17 00:00:00 2001 From: Christopher Brind Date: Wed, 28 Sep 2022 16:43:52 +0100 Subject: [PATCH 03/27] add explicit start method to configuration manager (#723) Add start function to explicitly start the configuration manager rather than have it start as a side effect of accessing the singleton. --- DuckDuckGo/App Delegate/AppDelegate.swift | 2 +- DuckDuckGo/Configuration/ConfigurationManager.swift | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/DuckDuckGo/App Delegate/AppDelegate.swift b/DuckDuckGo/App Delegate/AppDelegate.swift index 276f829826..0a98c6ad0f 100644 --- a/DuckDuckGo/App Delegate/AppDelegate.swift +++ b/DuckDuckGo/App Delegate/AppDelegate.swift @@ -90,7 +90,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate { PrivacyFeatures.httpsUpgrade.loadDataAsync() LocalBookmarkManager.shared.loadBookmarks() FaviconManager.shared.loadFavicons() - _ = ConfigurationManager.shared + ConfigurationManager.shared.start() _ = DownloadListCoordinator.shared _ = RecentlyClosedCoordinator.shared diff --git a/DuckDuckGo/Configuration/ConfigurationManager.swift b/DuckDuckGo/Configuration/ConfigurationManager.swift index 145015ad7f..6978977980 100644 --- a/DuckDuckGo/Configuration/ConfigurationManager.swift +++ b/DuckDuckGo/Configuration/ConfigurationManager.swift @@ -59,7 +59,9 @@ final class ConfigurationManager { /// Use the shared instance if subscribing to events. Only use the constructor for testing. init(configDownloader: ConfigurationDownloading = DefaultConfigurationDownloader(deliveryQueue: ConfigurationManager.queue)) { self.configDownloader = configDownloader + } + func start() { os_log("Starting configuration refresh timer", log: .config, type: .debug) timerCancellable = Timer.publish(every: Constants.refreshCheckIntervalSeconds, on: .main, in: .default) .autoconnect() @@ -68,6 +70,7 @@ final class ConfigurationManager { self.lastRefreshCheckTime = Date() self.refreshIfNeeded() }) + refreshNow() } func log() { From 180dc6e50a1b86ebc98e22fa24f70610fdb20c80 Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 28 Sep 2022 15:33:43 -0700 Subject: [PATCH 04/27] Address macOS keychain permission failures (#721) Task/Issue URL: https://app.asana.com/0/1177771139624306/1202974804358541/f Tech Design URL: CC: Description: This PR migrates keychain data to the data protection keychain. See duckduckgo/BrowserServicesKit#141 for more information. --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../ContentOverlayViewController.swift | 25 ++++++++++++----- DuckDuckGo/Common/Utilities/Logging.swift | 4 +-- .../Email/EmailManagerRequestDelegate.swift | 27 +++++++++++++------ 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 3518b355e9..d08396040d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -6238,7 +6238,7 @@ repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 30.0.0; + version = 31.0.0; }; }; AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = { diff --git a/DuckDuckGo/Autofill/ContentOverlayViewController.swift b/DuckDuckGo/Autofill/ContentOverlayViewController.swift index 4e7480958e..84ec3c2348 100644 --- a/DuckDuckGo/Autofill/ContentOverlayViewController.swift +++ b/DuckDuckGo/Autofill/ContentOverlayViewController.swift @@ -163,14 +163,25 @@ public final class ContentOverlayViewController: NSViewController, EmailManagerR public func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) { var parameters = [ - "access_type": accessType.rawValue, - "error": error.errorDescription - ] - - if case let .keychainAccessFailure(status) = error { - parameters["keychain_status"] = String(status) - } + "access_type": accessType.rawValue, + "error": error.errorDescription + ] + + if case let .keychainLookupFailure(status) = error { + parameters["keychain_status"] = String(status) + parameters["keychain_operation"] = "lookup" + } + if case let .keychainDeleteFailure(status) = error { + parameters["keychain_status"] = String(status) + parameters["keychain_operation"] = "delete" + } + + if case let .keychainSaveFailure(status) = error { + parameters["keychain_status"] = String(status) + parameters["keychain_operation"] = "save" + } + Pixel.fire(.debug(event: .emailAutofillKeychainError), withAdditionalParameters: parameters) } diff --git a/DuckDuckGo/Common/Utilities/Logging.swift b/DuckDuckGo/Common/Utilities/Logging.swift index b13270b6d5..5aea1e29fa 100644 --- a/DuckDuckGo/Common/Utilities/Logging.swift +++ b/DuckDuckGo/Common/Utilities/Logging.swift @@ -109,10 +109,10 @@ struct Logging { fileprivate static let autoconsentLoggingEnabled = false fileprivate static let autoconsentLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Autoconsent") - fileprivate static let bookmarksLoggingEnabled = true + fileprivate static let bookmarksLoggingEnabled = false fileprivate static let bookmarksLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Bookmarks") - fileprivate static let attributionLoggingEnabled = true + fileprivate static let attributionLoggingEnabled = false fileprivate static let attributionLog: OSLog = OSLog(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "Ad Attribution") } diff --git a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift index a0d77bddbb..c80ace03fe 100644 --- a/DuckDuckGo/Email/EmailManagerRequestDelegate.swift +++ b/DuckDuckGo/Email/EmailManagerRequestDelegate.swift @@ -45,15 +45,26 @@ extension EmailManagerRequestDelegate { } // swiftlint:enable function_parameter_count - public func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) { + public func emailManagerKeychainAccessFailed(accessType: EmailKeychainAccessType, error: EmailKeychainAccessError) { var parameters = [ - "access_type": accessType.rawValue, - "error": error.errorDescription - ] - - if case let .keychainAccessFailure(status) = error { - parameters["keychain_status"] = String(status) - } + "access_type": accessType.rawValue, + "error": error.errorDescription + ] + + if case let .keychainLookupFailure(status) = error { + parameters["keychain_status"] = String(status) + parameters["keychain_operation"] = "lookup" + } + + if case let .keychainDeleteFailure(status) = error { + parameters["keychain_status"] = String(status) + parameters["keychain_operation"] = "delete" + } + + if case let .keychainSaveFailure(status) = error { + parameters["keychain_status"] = String(status) + parameters["keychain_operation"] = "save" + } Pixel.fire(.debug(event: .emailAutofillKeychainError), withAdditionalParameters: parameters) } From add6b8bfddb5faf464315e549b98c0bd1e82294a Mon Sep 17 00:00:00 2001 From: Sam Symons Date: Wed, 28 Sep 2022 18:37:13 -0700 Subject: [PATCH 05/27] Add pinning support for Autofill and Bookmarks (#706) Task/Issue URL: https://app.asana.com/0/1199230911884351/1202896016091153/f Tech Design URL: CC: Description: This PR adds new options to the View menu and navigation bar context menu to allow users to keep the Autofill, Bookmarks, and Downloads button on the navigation bar permanently. --- DuckDuckGo.xcodeproj/project.pbxproj | 36 ++-- .../View/BookmarkListViewController.swift | 8 + .../Bookmarks/View/Bookmarks.storyboard | 195 ++++++++++-------- DuckDuckGo/Common/Localizables/UserText.swift | 26 ++- .../Utilities/UserDefaultsWrapper.swift | 2 + .../Common/View/AppKit/MouseOverButton.swift | 11 +- .../File Download/View/Downloads.storyboard | 6 +- .../View/DownloadsViewController.swift | 6 + DuckDuckGo/Menus/MainMenu.storyboard | 31 ++- DuckDuckGo/Menus/MainMenu.swift | 10 + DuckDuckGo/Menus/MainMenuActions.swift | 12 ++ .../Navigation Bar/PinningManager.swift | 75 +++++++ .../View/NavigationBar.storyboard | 22 +- .../View/NavigationBarViewController.swift | 133 ++++++++++-- .../PasswordManagementViewController.swift | 3 + .../View/PasswordManager.storyboard | 6 +- .../LocalPinningManagerTests.swift | 72 +++++++ 17 files changed, 512 insertions(+), 142 deletions(-) create mode 100644 DuckDuckGo/Navigation Bar/PinningManager.swift create mode 100644 Unit Tests/Navigation Bar/LocalPinningManagerTests.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index d08396040d..ec978fcf40 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -107,6 +107,7 @@ 4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; }; 4B0511E2262CAA8600F6079C /* NSViewControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */; }; 4B0511E7262CAB3700F6079C /* UserDefaultsWrapperUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511E6262CAB3700F6079C /* UserDefaultsWrapperUtilities.swift */; }; + 4B0DB5E528BD9D08007DD239 /* PinningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0DB5E428BD9D08007DD239 /* PinningManager.swift */; }; 4B11060525903E570039B979 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 4B11060325903E570039B979 /* CoreDataEncryptionTesting.xcdatamodeld */; }; 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B11060925903EAC0039B979 /* CoreDataEncryptionTests.swift */; }; 4B117F7D276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B117F7C276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift */; }; @@ -226,6 +227,7 @@ 4B9292D92667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */; }; 4B9292DB2667125D00AD2C21 /* ContextualMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292DA2667125D00AD2C21 /* ContextualMenu.swift */; }; 4B980E212817604000282EE1 /* NSNotificationName+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */; }; + 4B98D28028D9722A003C2B6F /* RecentlyVisitedSiteModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B98D27F28D9722A003C2B6F /* RecentlyVisitedSiteModelTests.swift */; }; 4BA1A69B258B076900F6F690 /* FileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA1A69A258B076900F6F690 /* FileStore.swift */; }; 4BA1A6A0258B079600F6F690 /* DataEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA1A69F258B079600F6F690 /* DataEncryption.swift */; }; 4BA1A6A5258B07DF00F6F690 /* EncryptedValueTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA1A6A4258B07DF00F6F690 /* EncryptedValueTransformer.swift */; }; @@ -295,7 +297,7 @@ 4BF01C00272AE74C00884A61 /* CountryList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE65482271FCD53008D1D63 /* CountryList.swift */; }; 4BF4951826C08395000547B8 /* ThirdPartyBrowserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF4951726C08395000547B8 /* ThirdPartyBrowserTests.swift */; }; 4BF4EA5027C71F26004E57C4 /* PasswordManagementListSectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF4EA4F27C71F26004E57C4 /* PasswordManagementListSectionTests.swift */; }; - 4BF6961D28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF6961C28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift */; }; + 4BF6962028BEEE8B00D402D4 /* LocalPinningManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BF6961F28BEEE8B00D402D4 /* LocalPinningManagerTests.swift */; }; 7B1E819E27C8874900FF0E60 /* ContentOverlayPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */; }; 7B1E819F27C8874900FF0E60 /* ContentOverlay.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */; }; 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */; }; @@ -921,6 +923,7 @@ 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSOpenPanelExtensions.swift; sourceTree = ""; }; 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSViewControllerExtension.swift; sourceTree = ""; }; 4B0511E6262CAB3700F6079C /* UserDefaultsWrapperUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWrapperUtilities.swift; sourceTree = ""; }; + 4B0DB5E428BD9D08007DD239 /* PinningManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinningManager.swift; sourceTree = ""; }; 4B11060425903E570039B979 /* CoreDataEncryptionTesting.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = CoreDataEncryptionTesting.xcdatamodel; sourceTree = ""; }; 4B11060925903EAC0039B979 /* CoreDataEncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataEncryptionTests.swift; sourceTree = ""; }; 4B117F7C276C0CB5002F3D8C /* LocalStatisticsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalStatisticsStoreTests.swift; sourceTree = ""; }; @@ -1041,6 +1044,7 @@ 4B9292D82667124B00AD2C21 /* BookmarkListTreeControllerDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerDataSource.swift; sourceTree = ""; }; 4B9292DA2667125D00AD2C21 /* ContextualMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextualMenu.swift; sourceTree = ""; }; 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNotificationName+Debug.swift"; sourceTree = ""; }; + 4B98D27F28D9722A003C2B6F /* RecentlyVisitedSiteModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentlyVisitedSiteModelTests.swift; sourceTree = ""; }; 4BA1A69A258B076900F6F690 /* FileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStore.swift; sourceTree = ""; }; 4BA1A69F258B079600F6F690 /* DataEncryption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataEncryption.swift; sourceTree = ""; }; 4BA1A6A4258B07DF00F6F690 /* EncryptedValueTransformer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedValueTransformer.swift; sourceTree = ""; }; @@ -1111,7 +1115,7 @@ 4BEF0E712766B11200AF7C58 /* MacWaitlistLockScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacWaitlistLockScreenViewModel.swift; sourceTree = ""; }; 4BF4951726C08395000547B8 /* ThirdPartyBrowserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartyBrowserTests.swift; sourceTree = ""; }; 4BF4EA4F27C71F26004E57C4 /* PasswordManagementListSectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordManagementListSectionTests.swift; sourceTree = ""; }; - 4BF6961C28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyVisitedSiteModelTests.swift; sourceTree = ""; }; + 4BF6961F28BEEE8B00D402D4 /* LocalPinningManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalPinningManagerTests.swift; sourceTree = ""; }; 7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayPopover.swift; sourceTree = ""; }; 7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = ContentOverlay.storyboard; sourceTree = ""; }; 7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentOverlayViewController.swift; sourceTree = ""; }; @@ -2214,6 +2218,14 @@ path = Extensions; sourceTree = ""; }; + 4B98D27E28D9722A003C2B6F /* Home Page */ = { + isa = PBXGroup; + children = ( + 4B98D27F28D9722A003C2B6F /* RecentlyVisitedSiteModelTests.swift */, + ); + path = "Home Page"; + sourceTree = ""; + }; 4BA1A691258B06F600F6F690 /* File System */ = { isa = PBXGroup; children = ( @@ -2358,15 +2370,7 @@ path = Model; sourceTree = ""; }; - 4BF6961B28BE90E800D402D4 /* Home Page */ = { - isa = PBXGroup; - children = ( - 4BF6961C28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift */, - ); - path = "Home Page"; - sourceTree = ""; - }; - 4BF6962128C242E500D402D4 /* Autoconsent */ = { + 4BF6961B28BE90E800D402D4 /* Autoconsent */ = { isa = PBXGroup; children = ( FD23FD2A28816606007F6985 /* AutoconsentMessageProtocolTests.swift */, @@ -2895,7 +2899,6 @@ children = ( B6A5A28C25B962CB00AA7ADA /* App */, 85F1B0C725EF9747004792B6 /* App Delegate */, - 4BF6962128C242E500D402D4 /* Autoconsent */, AA652CAB25DD820D009059CC /* Bookmarks */, 4B43468D285ED6BD00177407 /* Bookmarks Bar */, AA92ACAE24EFE1F5005F41C9 /* Browser Tab */, @@ -2911,7 +2914,8 @@ 4B02199725E063DE00ED7DEA /* Fireproofing */, B68172AC269EB415006D1092 /* Geolocation */, AAEC74AE2642C47300C2EFBC /* History */, - 4BF6961B28BE90E800D402D4 /* Home Page */, + 4B98D27E28D9722A003C2B6F /* Home Page */, + 4BF6961B28BE90E800D402D4 /* Autoconsent */, 378205F9283C275E00D1D4AA /* Menus */, AA91F83627076ED100771A0D /* Navigation Bar */, 85F487B3276A8F1B003CE668 /* Onboarding */, @@ -3195,6 +3199,7 @@ 853014D425E6709500FB8205 /* Support */, AA86491624D8339A001BABEE /* View */, AAA0CC3A25337F990079BC96 /* ViewModel */, + 4B0DB5E428BD9D08007DD239 /* PinningManager.swift */, ); path = "Navigation Bar"; sourceTree = ""; @@ -3279,6 +3284,7 @@ isa = PBXGroup; children = ( AA91F83727076EEE00771A0D /* ViewModel */, + 4BF6961F28BEEE8B00D402D4 /* LocalPinningManagerTests.swift */, ); path = "Navigation Bar"; sourceTree = ""; @@ -4850,6 +4856,7 @@ AA9FF95B24A1EFC20039E328 /* TabViewModel.swift in Sources */, AA9E9A5E25A4867200D1959D /* TabDragAndDropManager.swift in Sources */, 4BBD3C00285ACE090047A89D /* NSNotificationName+Favicons.swift in Sources */, + 4B0DB5E528BD9D08007DD239 /* PinningManager.swift in Sources */, B68458C025C7E9E000DC17B6 /* TabCollectionViewModel+NSSecureCoding.swift in Sources */, AA8EDF2724923EC70071C2E8 /* StringExtension.swift in Sources */, 85378DA2274E7F25007C5CBF /* EmailManagerRequestDelegate.swift in Sources */, @@ -5008,7 +5015,6 @@ 37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */, 85F1B0C925EF9759004792B6 /* URLEventHandlerTests.swift in Sources */, 4B9292BD2667103100AD2C21 /* BookmarkOutlineViewDataSourceTests.swift in Sources */, - 4BF6961D28BE911100D402D4 /* RecentlyVisitedSiteModelTests.swift in Sources */, 4BBF0917282DD6EF00EE1418 /* TemporaryFileHandlerTests.swift in Sources */, B6A5A27925B93FFF00AA7ADA /* StateRestorationManagerTests.swift in Sources */, 4B9292BB2667103100AD2C21 /* BookmarkNodeTests.swift in Sources */, @@ -5063,6 +5069,7 @@ B63ED0E326B3E7FA00A9DAD1 /* CLLocationManagerMock.swift in Sources */, 37CD54BB27F25A4000F1F7B9 /* DownloadsPreferencesTests.swift in Sources */, 4B02199C25E063DE00ED7DEA /* FireproofDomainsTests.swift in Sources */, + 4B98D28028D9722A003C2B6F /* RecentlyVisitedSiteModelTests.swift in Sources */, AA0F3DB7261A566C0077F2D9 /* SuggestionLoadingMock.swift in Sources */, 4B9292BE2667103100AD2C21 /* PasteboardFolderTests.swift in Sources */, 4B9292C52667104B00AD2C21 /* CoreDataTestUtilities.swift in Sources */, @@ -5113,6 +5120,7 @@ B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */, AAEC74BB2642E67C00C2EFBC /* NSPersistentContainerExtension.swift in Sources */, AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, + 4BF6962028BEEE8B00D402D4 /* LocalPinningManagerTests.swift in Sources */, AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */, 4BA1A6E6258C270800F6F690 /* EncryptionKeyGeneratorTests.swift in Sources */, B6106BB326A7F4AA0013B453 /* GeolocationServiceMock.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift index 59e0b296a9..2d5191ea8b 100644 --- a/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift +++ b/DuckDuckGo/Bookmarks/View/BookmarkListViewController.swift @@ -45,6 +45,10 @@ final class BookmarkListViewController: NSViewController { @IBOutlet var emptyState: NSView! @IBOutlet var emptyStateTitle: NSTextField! @IBOutlet var emptyStateMessage: NSTextField! + + @IBOutlet var newBookmarkButton: NSButton! + @IBOutlet var newFolderButton: NSButton! + @IBOutlet var manageBookmarksButton: NSButton! private var cancellables = Set() private var bookmarkManager: BookmarkManager = LocalBookmarkManager.shared @@ -91,6 +95,10 @@ final class BookmarkListViewController: NSViewController { emptyStateTitle.attributedStringValue = NSAttributedString.make(emptyStateTitle.stringValue, lineHeight: 1.14, kern: -0.23) emptyStateMessage.attributedStringValue = NSAttributedString.make(emptyStateMessage.stringValue, lineHeight: 1.05, kern: -0.08) + + newBookmarkButton.toolTip = UserText.newBookmarkTooltip + newFolderButton.toolTip = UserText.newFolderTooltip + manageBookmarksButton.toolTip = UserText.manageBookmarksTooltip } override func viewWillAppear() { diff --git a/DuckDuckGo/Bookmarks/View/Bookmarks.storyboard b/DuckDuckGo/Bookmarks/View/Bookmarks.storyboard index ee9f27d64d..b22407017e 100644 --- a/DuckDuckGo/Bookmarks/View/Bookmarks.storyboard +++ b/DuckDuckGo/Bookmarks/View/Bookmarks.storyboard @@ -1,8 +1,8 @@ - + - + @@ -714,84 +714,108 @@ Gw - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -901,20 +925,16 @@ Gw - - + - - - - + @@ -922,6 +942,9 @@ Gw + + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index b50091b3d9..4b6ec17717 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -539,5 +539,29 @@ struct UserText { static let bookmarksBarContextMenuCopy = NSLocalizedString("bookmarks.bar.context-menu.copy", value: "Copy", comment: "Copy menu item for the bookmarks bar context menu") static let bookmarksBarContextMenuDelete = NSLocalizedString("bookmarks.bar.context-menu.delete", value: "Delete", comment: "Delete menu item for the bookmarks bar context menu") static let bookmarksBarContextMenuMoveToEnd = NSLocalizedString("bookmarks.bar.context-menu.move-to-end", value: "Move to End", comment: "Move to End menu item for the bookmarks bar context menu") - + + static let showAutofillShortcut = NSLocalizedString("pinning.show-autofill-shortcut", value: "Show Autofill Shortcut", comment: "Menu item for showing the autofill shortcut") + static let hideAutofillShortcut = NSLocalizedString("pinning.hide-autofill-shortcut", value: "Hide Autofill Shortcut", comment: "Menu item for hiding the autofill shortcut") + + static let showBookmarksShortcut = NSLocalizedString("pinning.show-bookmarks-shortcut", value: "Show Bookmarks Shortcut", comment: "Menu item for showing the bookmarks shortcut") + static let hideBookmarksShortcut = NSLocalizedString("pinning.hide-bookmarks-shortcut", value: "Hide Bookmarks Shortcut", comment: "Menu item for hiding the bookmarks shortcut") + + static let showDownloadsShortcut = NSLocalizedString("pinning.show-downloads-shortcut", value: "Show Downloads Shortcut", comment: "Menu item for showing the downloads shortcut") + static let hideDownloadsShortcut = NSLocalizedString("pinning.hide-downloads-shortcut", value: "Hide Downloads Shortcut", comment: "Menu item for hiding the downloads shortcut") + + // MARK: - Tooltips + + static let autofillShortcutTooltip = NSLocalizedString("tooltip.autofill.shortcut", value: "Autofill", comment: "Tooltip for the autofill shortcut") + static let bookmarksShortcutTooltip = NSLocalizedString("tooltip.bookmarks.shortcut", value: "Bookmarks", comment: "Tooltip for the bookmarks shortcut") + static let downloadsShortcutTooltip = NSLocalizedString("tooltip.downloads.shortcut", value: "Downloads", comment: "Tooltip for the downloads shortcut") + + static let addItemTooltip = NSLocalizedString("tooltip.autofill.add-item", value: "Add item", comment: "Tooltip for the Add Item button") + static let moreOptionsTooltip = NSLocalizedString("tooltip.autofill.more-options", value: "More options", comment: "Tooltip for the More Options button") + + static let newBookmarkTooltip = NSLocalizedString("tooltip.bookmarks.new-bookmark", value: "New bookmark", comment: "Tooltip for the New Bookmark button") + static let newFolderTooltip = NSLocalizedString("tooltip.bookmarks.new-folder", value: "New folder", comment: "Tooltip for the New Folder button") + static let manageBookmarksTooltip = NSLocalizedString("tooltip.bookmarks.manage-bookmarks", value: "Manage bookmarks", comment: "Tooltip for the Manage Bookmarks button") + + static let openDownloadsFolderTooltip = NSLocalizedString("tooltip.downloads.open-downloads-folder", value: "Open downloads folder", comment: "Tooltip for the Open Downloads Folder button") + static let clearDownloadHistoryTooltip = NSLocalizedString("tooltip.downloads.clear-download-history", value: "Clear download history", comment: "Tooltip for the Clear Downloads button") } diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 32c667bd87..cd7ddb5a99 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -84,6 +84,8 @@ public struct UserDefaultsWrapper { case historyV5toV6Migration = "history.v5.to.v6.migration.2" case showBookmarksBar = "bookmarks.bar.show" + + case pinnedViews = "pinning.pinned-views" } enum RemovedKeys: String, CaseIterable { diff --git a/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift b/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift index 3bb96bc402..755da6de53 100644 --- a/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift +++ b/DuckDuckGo/Common/View/AppKit/MouseOverButton.swift @@ -22,11 +22,18 @@ internal class MouseOverButton: NSButton { let backgroundLayer = CALayer() + @IBInspectable var backgroundColor: NSColor? { + didSet { + updateLayer() + } + } + @IBInspectable var mouseOverColor: NSColor? { didSet { updateLayer() } } + @IBInspectable var mouseDownColor: NSColor? { didSet { updateLayer() @@ -38,11 +45,13 @@ internal class MouseOverButton: NSButton { updateTintColor() } } + @IBInspectable var mouseOverTintColor: NSColor? { didSet { updateTintColor() } } + @IBInspectable var mouseDownTintColor: NSColor? { didSet { updateTintColor() @@ -162,7 +171,7 @@ internal class MouseOverButton: NSButton { context.duration = 0.0 backgroundLayer.backgroundColor = mouseOverColor?.cgColor ?? NSColor.clear.cgColor } else { - backgroundLayer.backgroundColor = NSColor.clear.cgColor + backgroundLayer.backgroundColor = backgroundColor?.cgColor ?? NSColor.clear.cgColor } } } diff --git a/DuckDuckGo/File Download/View/Downloads.storyboard b/DuckDuckGo/File Download/View/Downloads.storyboard index 77c8347c58..82719deba8 100644 --- a/DuckDuckGo/File Download/View/Downloads.storyboard +++ b/DuckDuckGo/File Download/View/Downloads.storyboard @@ -1,8 +1,8 @@ - + - + @@ -378,7 +378,9 @@ + + diff --git a/DuckDuckGo/File Download/View/DownloadsViewController.swift b/DuckDuckGo/File Download/View/DownloadsViewController.swift index db1909a1d5..78325163f6 100644 --- a/DuckDuckGo/File Download/View/DownloadsViewController.swift +++ b/DuckDuckGo/File Download/View/DownloadsViewController.swift @@ -36,6 +36,9 @@ final class DownloadsViewController: NSViewController { return controller } + @IBOutlet var openDownloadsFolderButton: NSButton! + @IBOutlet var clearDownloadsButton: NSButton! + @IBOutlet var contextMenu: NSMenu! @IBOutlet var tableView: NSTableView! @IBOutlet var tableViewHeightConstraint: NSLayoutConstraint? @@ -50,6 +53,9 @@ final class DownloadsViewController: NSViewController { super.viewDidLoad() setupDragAndDrop() + + openDownloadsFolderButton.toolTip = UserText.openDownloadsFolderTooltip + clearDownloadsButton.toolTip = UserText.clearDownloadHistoryTooltip } override func viewWillAppear() { diff --git a/DuckDuckGo/Menus/MainMenu.storyboard b/DuckDuckGo/Menus/MainMenu.storyboard index a2422a4ccc..e351ee71c7 100644 --- a/DuckDuckGo/Menus/MainMenu.storyboard +++ b/DuckDuckGo/Menus/MainMenu.storyboard @@ -1,8 +1,8 @@ - + - + @@ -385,17 +385,33 @@ - + - + - + - + + + + + + + + + + + + + + + + + @@ -812,7 +828,10 @@ CQ + + + diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index c01a3cf836..df0cdc99da 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -69,6 +69,9 @@ final class MainMenu: NSMenu { @IBOutlet weak var favoriteThisPageMenuItem: NSMenuItem? @IBOutlet weak var toggleBookmarksBarMenuItem: NSMenuItem? + @IBOutlet weak var toggleAutofillShortcutMenuItem: NSMenuItem? + @IBOutlet weak var toggleBookmarksShortcutMenuItem: NSMenuItem? + @IBOutlet weak var toggleDownloadsShortcutMenuItem: NSMenuItem? // MARK: - Debug @IBOutlet weak var debugMenuItem: NSMenuItem? { @@ -106,6 +109,7 @@ final class MainMenu: NSMenu { shareMenuItem.submenu = sharingMenu updateBookmarksBarMenuItem() + updateShortcutMenuItems() } private func setup() { @@ -236,6 +240,12 @@ final class MainMenu: NSMenu { toggleBookmarksBarMenuItem?.title = title bookmarksMenuToggleBookmarksBarMenuItem?.title = title } + + private func updateShortcutMenuItems() { + toggleAutofillShortcutMenuItem?.title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .autofill) + toggleBookmarksShortcutMenuItem?.title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .bookmarks) + toggleDownloadsShortcutMenuItem?.title = LocalPinningManager.shared.toggleShortcutInterfaceTitle(for: .downloads) + } } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 9066d03644..c6c0a0829d 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -318,6 +318,18 @@ extension MainViewController { @IBAction func toggleBookmarksBar(_ sender: Any) { PersistentAppInterfaceSettings.shared.showBookmarksBar.toggle() } + + @IBAction func toggleAutofillShortcut(_ sender: Any) { + LocalPinningManager.shared.togglePinning(for: .autofill) + } + + @IBAction func toggleBookmarksShortcut(_ sender: Any) { + LocalPinningManager.shared.togglePinning(for: .bookmarks) + } + + @IBAction func toggleDownloadsShortcut(_ sender: Any) { + LocalPinningManager.shared.togglePinning(for: .downloads) + } // MARK: - History diff --git a/DuckDuckGo/Navigation Bar/PinningManager.swift b/DuckDuckGo/Navigation Bar/PinningManager.swift new file mode 100644 index 0000000000..5a4ac6bf46 --- /dev/null +++ b/DuckDuckGo/Navigation Bar/PinningManager.swift @@ -0,0 +1,75 @@ +// +// PinningManager.swift +// +// Copyright © 2022 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum PinnableView: String { + case autofill + case bookmarks + case downloads +} + +protocol PinningManager { + + func togglePinning(for view: PinnableView) + func isPinned(_ view: PinnableView) -> Bool + +} + +final class LocalPinningManager: PinningManager { + + static let shared = LocalPinningManager() + + static let pinnedViewChangedNotificationViewTypeKey = "pinning.pinnedViewChanged.viewType" + + @UserDefaultsWrapper(key: .pinnedViews, defaultValue: []) + private var pinnedViewStrings: [String] + + func togglePinning(for view: PinnableView) { + if isPinned(view) { + pinnedViewStrings.removeAll(where: { $0 == view.rawValue }) + } else { + pinnedViewStrings.append(view.rawValue) + } + + NotificationCenter.default.post(name: .PinnedViewsChanged, object: nil, userInfo: [ + Self.pinnedViewChangedNotificationViewTypeKey: view.rawValue + ]) + } + + func isPinned(_ view: PinnableView) -> Bool { + return pinnedViewStrings.contains(view.rawValue) + } + + func toggleShortcutInterfaceTitle(for view: PinnableView) -> String { + switch view { + case .autofill: return isPinned(.autofill) ? UserText.hideAutofillShortcut : UserText.showAutofillShortcut + case .bookmarks: return isPinned(.bookmarks) ? UserText.hideBookmarksShortcut : UserText.showBookmarksShortcut + case .downloads: return isPinned(.downloads) ? UserText.hideDownloadsShortcut : UserText.showDownloadsShortcut + } + } + +} + +// MARK: - NSNotification + +extension NSNotification.Name { + + static let PinnedViewsChanged = NSNotification.Name("pinning.pinnedViewsChanged") + +} diff --git a/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard b/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard index 818a4f912d..efc419a5d8 100644 --- a/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard +++ b/DuckDuckGo/Navigation Bar/View/NavigationBar.storyboard @@ -143,13 +143,13 @@ - - - +