diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index cf6f987387..54382a2539 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -637,7 +637,6 @@ 3706FC8C293F65D500E42796 /* FirefoxDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B5FF67726B602B100D42879 /* FirefoxDataImporter.swift */; }; 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8A27DB69BC00471A10 /* PreferencesGeneralView.swift */; }; 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF3F1F286F0A7A00BD9014 /* PinnedTabsView.swift */; }; - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37AFCE8027DA2CA600471A10 /* PreferencesViewController.swift */; }; 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B02198125E05FAC00ED7DEA /* FireproofDomains.swift */; }; 3706FC95293F65D500E42796 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B677440255DBEEA00025BD8 /* Database.swift */; }; @@ -1361,7 +1360,6 @@ 4BD57C042AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BD57C052AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */; }; 4BDFA4AE27BF19E500648192 /* ToggleableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */; }; - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */; }; 4BE344EE2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE344EF2B23786F003FC223 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */; }; 4BE4005327CF3DC3007D3161 /* SavePaymentMethodPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */; }; @@ -3216,7 +3214,6 @@ 4BD18F04283F151F00058124 /* BookmarksBar.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BookmarksBar.storyboard; sourceTree = ""; }; 4BD57C032AC112DF00B580EE /* NetworkProtectionRemoteMessagingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkProtectionRemoteMessagingTests.swift; sourceTree = ""; }; 4BDFA4AD27BF19E500648192 /* ToggleableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableScrollView.swift; sourceTree = ""; }; - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSStoryboardExtension.swift; sourceTree = ""; }; 4BE344ED2B2376DF003FC223 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = ""; }; 4BE4005227CF3DC3007D3161 /* SavePaymentMethodPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodPopover.swift; sourceTree = ""; }; 4BE4005427CF3F19007D3161 /* SavePaymentMethodViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SavePaymentMethodViewController.swift; sourceTree = ""; }; @@ -7357,7 +7354,6 @@ B6B3E0DC2657E9CF0040E0A2 /* NSScreenExtension.swift */, AAC5E4E325D6BA9C007F5990 /* NSSizeExtension.swift */, 4B39AAF527D9B2C700A73FD5 /* NSStackViewExtension.swift */, - 4BE0DF0426781961006337B7 /* NSStoryboardExtension.swift */, AA5C8F58258FE21F00748EB7 /* NSTextFieldExtension.swift */, 858A798426A8BB5D00A75A42 /* NSTextViewExtension.swift */, 4B0511E0262CAA8600F6079C /* NSViewControllerExtension.swift */, @@ -10122,7 +10118,6 @@ 3706FC8D293F65D500E42796 /* PreferencesGeneralView.swift in Sources */, 37197EA92942443D00394917 /* WebView.swift in Sources */, 3706FC8E293F65D500E42796 /* PinnedTabsView.swift in Sources */, - 3706FC92293F65D500E42796 /* NSStoryboardExtension.swift in Sources */, B6B5F58A2B03673B008DB58A /* BrowserImportMoreInfoView.swift in Sources */, 3706FC93293F65D500E42796 /* PreferencesViewController.swift in Sources */, 3706FC94293F65D500E42796 /* FireproofDomains.swift in Sources */, @@ -11464,7 +11459,6 @@ AA8EDF2424923E980071C2E8 /* URLExtension.swift in Sources */, B634DBDF293C8F7F00C3C99E /* Tab+UIDelegate.swift in Sources */, B6F1B02E2BCE6B47005E863C /* TunnelControllerProvider.swift in Sources */, - 4BE0DF06267819A1006337B7 /* NSStoryboardExtension.swift in Sources */, 37AFCE8127DA2CA600471A10 /* PreferencesViewController.swift in Sources */, 4B02198A25E05FAC00ED7DEA /* FireproofDomains.swift in Sources */, 4B677442255DBEEA00025BD8 /* Database.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg new file mode 100644 index 0000000000..2b4a602355 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg new file mode 100644 index 0000000000..4faab69801 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Chevron-Right-12.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json new file mode 100644 index 0000000000..7fea6d5282 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Chevron-Right-12.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "Chevron-Right-12.svg", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "Chevron-Right-12-light.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..c878d4b14f --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Identity-Theft-Restoration-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf new file mode 100644 index 0000000000..30563fa583 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Identity-Theft-Restoration-Multicolor-16.imageset/Identity-Theft-Restoration-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..a30ef3d53e --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "PersonalInformationRemoval-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf new file mode 100644 index 0000000000..fbabea4523 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/PersonalInformationRemoval-Multicolor-16.imageset/PersonalInformationRemoval-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json new file mode 100644 index 0000000000..d4b2052646 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Settings-Multicolor-16.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf new file mode 100644 index 0000000000..b3b41002c2 Binary files /dev/null and b/DuckDuckGo/Assets.xcassets/Images/Settings-Multicolor-16.imageset/Settings-Multicolor-16.pdf differ diff --git a/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift b/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift index 01f2d1c255..58a1cd7fb0 100644 --- a/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSAttributedStringExtension.swift @@ -18,6 +18,7 @@ import AppKit +typealias NSAttributedStringBuilder = ArrayBuilder extension NSAttributedString { /// These values come from Figma. Click on the text in Figma and choose Code > iOS to see the values. @@ -31,6 +32,31 @@ extension NSAttributedString { ]) } + convenience init(image: NSImage, rect: CGRect) { + let attachment = NSTextAttachment() + attachment.image = image + attachment.bounds = rect + self.init(attachment: attachment) + } + + convenience init(@NSAttributedStringBuilder components: () -> [NSAttributedString]) { + let components = components() + guard !components.isEmpty else { + self.init() + return + } + guard components.count > 1 else { + self.init(attributedString: components[0]) + return + } + let result = NSMutableAttributedString(attributedString: components[0]) + for component in components[1...] { + result.append(component) + } + + self.init(attributedString: result) + } + } extension NSMutableAttributedString { @@ -48,12 +74,3 @@ extension NSMutableAttributedString { } } - -extension NSTextAttachment { - func setImageHeight(height: CGFloat, offset: CGPoint = .zero) { - guard let image = image else { return } - let ratio = image.size.width / image.size.height - - bounds = CGRect(x: bounds.origin.x + offset.x, y: bounds.origin.y + offset.y, width: ratio * height, height: height) - } -} diff --git a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift b/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift deleted file mode 100644 index 9e50fa79ce..0000000000 --- a/DuckDuckGo/Common/Extensions/NSStoryboardExtension.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// NSStoryboardExtension.swift -// -// Copyright © 2021 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 AppKit - -extension NSStoryboard { - - static var bookmarks = NSStoryboard(name: "Bookmarks", bundle: .main) - -} diff --git a/DuckDuckGo/Common/Extensions/NSViewExtension.swift b/DuckDuckGo/Common/Extensions/NSViewExtension.swift index b82095c1f0..333dce57cf 100644 --- a/DuckDuckGo/Common/Extensions/NSViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/NSViewExtension.swift @@ -79,6 +79,11 @@ extension NSView { return self } + var isShown: Bool { + get { !isHidden } + set { isHidden = !newValue } + } + func makeMeFirstResponder() { guard let window = window else { os_log("%s: Window not available", type: .error, className) diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index abd290dd0f..f55e780cf3 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -142,7 +142,7 @@ extension URL { // base url for Error Page Alternate HTML loaded into Web View static let error = URL(string: "duck://error")! - static let dataBrokerProtection = URL(string: "duck://dbp")! + static let dataBrokerProtection = URL(string: "duck://personal-information-removal")! #if !SANDBOX_TEST_TOOL static func settingsPane(_ pane: PreferencePaneIdentifier) -> URL { @@ -409,6 +409,10 @@ extension URL { return false } + var isEmailProtection: Bool { + self.isChild(of: .duckDuckGoEmailLogin) || self == .duckDuckGoEmail + } + enum DuckDuckGoParameters: String { case search = "q" case ia @@ -552,7 +556,7 @@ extension URL { return false } - func stripUnsupportedCredentials() -> String { + func strippingUnsupportedCredentials() -> String { if self.absoluteString.firstIndex(of: "@") != nil { let authPattern = "([^:]+):\\/\\/[^\\/]*@" let strippedURL = self.absoluteString.replacingOccurrences(of: authPattern, with: "$1://", options: .regularExpression) @@ -563,7 +567,14 @@ extension URL { } public func isChild(of parentURL: URL) -> Bool { - guard let parentURLHost = parentURL.host, self.isPart(ofDomain: parentURLHost) else { return false } - return pathComponents.starts(with: parentURL.pathComponents) + if scheme == parentURL.scheme, + port == parentURL.port, + let parentURLHost = parentURL.host, + self.isPart(ofDomain: parentURLHost), + pathComponents.starts(with: parentURL.pathComponents) { + return true + } else { + return false + } } } diff --git a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift index 56f6b2de2b..9c444e511b 100644 --- a/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift +++ b/DuckDuckGo/Common/View/AppKit/LoadingProgressView.swift @@ -31,7 +31,7 @@ final class LoadingProgressView: NSView, CAAnimationDelegate { private var targetProgress: Double = 0.0 private var targetTime: CFTimeInterval = 0.0 - var isShown: Bool { + var isProgressShown: Bool { progressMask.opacity == 1.0 } diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index ec129e39df..eca7f8004d 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -938,8 +938,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(findInPage), #selector(findInPageNext), #selector(findInPagePrevious): - return activeTabViewModel?.canReload == true // must have content loaded - && view.window?.isKeyWindow == true // disable in full screen + return activeTabViewModel?.canFindInPage == true // must have content loaded + && view.window?.isKeyWindow == true // disable in video full screen case #selector(findInPageDone): return getActiveTabAndIndex()?.tab.findInPage?.isActive == true diff --git a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift index 878114b49b..f277f679b6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarButtonsViewController.swift @@ -275,7 +275,7 @@ final class AddressBarButtonsViewController: NSViewController { guard view.window?.isPopUpWindow == false else { return } bookmarkButton.setAccessibilityIdentifier("AddressBarButtonsViewController.bookmarkButton") let hasEmptyAddressBar = textFieldValue?.isEmpty ?? true - var showBookmarkButton: Bool { + var shouldShowBookmarkButton: Bool { guard let tabViewModel, tabViewModel.canBeBookmarked else { return false } var isUrlBookmarked = false @@ -287,7 +287,7 @@ final class AddressBarButtonsViewController: NSViewController { return clearButton.isHidden && !hasEmptyAddressBar && (isMouseOverNavigationBar || bookmarkPopover?.isShown == true || isUrlBookmarked) } - bookmarkButton.isHidden = !showBookmarkButton + bookmarkButton.isShown = shouldShowBookmarkButton } func openBookmarkPopover(setFavorite: Bool, accessPoint: GeneralPixel.AccessPoint) { @@ -299,7 +299,7 @@ final class AddressBarButtonsViewController: NSViewController { let bookmarkPopover = bookmarkPopoverCreatingIfNeeded() if !bookmarkPopover.isShown { - bookmarkButton.isHidden = false + bookmarkButton.isShown = true bookmarkPopover.isNew = result.isNew bookmarkPopover.bookmark = bookmark bookmarkPopover.show(positionedBelow: bookmarkButton) @@ -319,7 +319,7 @@ final class AddressBarButtonsViewController: NSViewController { }() if query.permissions.contains(.camera) - || (query.permissions.contains(.microphone) && microphoneButton.isHidden && !cameraButton.isHidden) { + || (query.permissions.contains(.microphone) && microphoneButton.isHidden && cameraButton.isShown) { button = cameraButton } else { assert(query.permissions.count == 1) @@ -342,9 +342,7 @@ final class AddressBarButtonsViewController: NSViewController { return } } - guard !button.isHidden, - !permissionButtons.isHidden - else { return } + guard button.isShown, permissionButtons.isShown else { return } (popover.contentViewController as? PermissionAuthorizationViewController)?.query = query popover.show(relativeTo: button.bounds, of: button, preferredEdge: .maxY) @@ -389,7 +387,7 @@ final class AddressBarButtonsViewController: NSViewController { func updateButtons() { stopAnimationsAfterFocus() - clearButton.isHidden = !(isTextFieldEditorFirstResponder && !(textFieldValue?.isEmpty ?? true)) + clearButton.isShown = isTextFieldEditorFirstResponder && !textFieldValue.isEmpty updatePrivacyEntryPointButton() updateImageButton() @@ -690,15 +688,15 @@ final class AddressBarButtonsViewController: NSViewController { } private func updatePermissionButtons() { - permissionButtons.isHidden = isTextFieldEditorFirstResponder - || isAnyTrackerAnimationPlaying - || (tabViewModel?.isShowingErrorPage ?? true) + guard let tabViewModel else { return } + + permissionButtons.isShown = !isTextFieldEditorFirstResponder + && !isAnyTrackerAnimationPlaying + && !tabViewModel.isShowingErrorPage defer { showOrHidePermissionPopoverIfNeeded() } - guard let tabViewModel else { return } - geolocationButton.buttonState = tabViewModel.usedPermissions.geolocation let (camera, microphone) = PermissionState?.combineCamera(tabViewModel.usedPermissions.camera, @@ -771,21 +769,24 @@ final class AddressBarButtonsViewController: NSViewController { guard let tabViewModel else { return } let url = tabViewModel.tab.content.userEditableUrl + let isNewTabOrOnboarding = [.newtab, .onboarding].contains(tabViewModel.tab.content) let isHypertextUrl = url?.navigationalScheme?.isHypertextScheme == true && url?.isDuckPlayer == false let isEditingMode = controllerMode?.isEditing ?? false let isTextFieldValueText = textFieldValue?.isText ?? false let isLocalUrl = url?.isLocalURL ?? false // Privacy entry point button - privacyEntryPointButton.isHidden = isEditingMode - || isTextFieldEditorFirstResponder - || !isHypertextUrl - || tabViewModel.isShowingErrorPage - || isTextFieldValueText - || isLocalUrl - imageButtonWrapper.isHidden = view.window?.isPopUpWindow == true - || !privacyEntryPointButton.isHidden - || isAnyTrackerAnimationPlaying + privacyEntryPointButton.isShown = !isEditingMode + && !isTextFieldEditorFirstResponder + && isHypertextUrl + && !tabViewModel.isShowingErrorPage + && !isTextFieldValueText + && !isLocalUrl + + imageButtonWrapper.isShown = view.window?.isPopUpWindow != true + && (isHypertextUrl || isTextFieldEditorFirstResponder || isEditingMode || isNewTabOrOnboarding) + && privacyEntryPointButton.isHidden + && !isAnyTrackerAnimationPlaying } private func updatePrivacyEntryPointIcon() { @@ -796,7 +797,7 @@ final class AddressBarButtonsViewController: NSViewController { guard !isAnyShieldAnimationPlaying else { return } switch tabViewModel.tab.content { - case .url(let url, _, _): + case .url(let url, _, _), .identityTheftRestoration(let url), .subscription(let url): guard let host = url.host else { break } let isNotSecure = url.scheme == URL.NavigationalScheme.http.rawValue @@ -824,8 +825,7 @@ final class AddressBarButtonsViewController: NSViewController { let trackerAnimationImageProvider = TrackerAnimationImageProvider() private func animateTrackers() { - guard !privacyEntryPointButton.isHidden, - let tabViewModel else { return } + guard privacyEntryPointButton.isShown, let tabViewModel else { return } switch tabViewModel.tab.content { case .url(let url, _, _): @@ -835,7 +835,7 @@ final class AddressBarButtonsViewController: NSViewController { } var animationView: LottieAnimationView - if url.scheme == "http" { + if url.navigationalScheme == .http { animationView = shieldDotAnimationView } else { animationView = shieldAnimationView @@ -878,7 +878,7 @@ final class AddressBarButtonsViewController: NSViewController { shieldAnimations: Bool = true, badgeAnimations: Bool = true) { func stopAnimation(_ animationView: LottieAnimationView) { - if animationView.isAnimationPlaying || !animationView.isHidden { + if animationView.isAnimationPlaying || animationView.isShown { animationView.isHidden = true animationView.stop() } diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift index c9692f1546..b4807c4cd6 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift @@ -856,6 +856,11 @@ extension AddressBarTextField { } } +extension AddressBarTextField.Value? { + var isEmpty: Bool { + self?.isEmpty ?? true + } +} // MARK: - NSTextFieldDelegate extension AddressBarTextField: NSTextFieldDelegate { diff --git a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift index cac827debd..f53910b5ec 100644 --- a/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/AddressBarViewController.swift @@ -225,9 +225,9 @@ final class AddressBarViewController: NSViewController { passiveTextField.stringValue = "" return } - tabViewModel.$passiveAddressBarString + tabViewModel.$passiveAddressBarAttributedString .receive(on: DispatchQueue.main) - .assign(to: \.stringValue, onWeaklyHeld: passiveTextField) + .assign(to: \.attributedStringValue, onWeaklyHeld: passiveTextField) .store(in: &tabViewModelCancellables) } @@ -259,7 +259,7 @@ final class AddressBarViewController: NSViewController { .sink { [weak self] value in guard tabViewModel.isLoading, let progressIndicator = self?.progressIndicator, - progressIndicator.isShown + progressIndicator.isProgressShown else { return } progressIndicator.increaseProgress(to: value) @@ -274,7 +274,7 @@ final class AddressBarViewController: NSViewController { if shouldShowLoadingIndicator(for: tabViewModel, isLoading: isLoading, error: error) { progressIndicator.show(progress: tabViewModel.progress, startTime: tabViewModel.loadingStartTime) - } else if progressIndicator.isShown { + } else if progressIndicator.isProgressShown { progressIndicator.finishAndHide() } } diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index bfc0a1025b..4d2dbb909a 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -421,12 +421,14 @@ final class MoreOptionsMenu: NSMenu { .withImage(image) } - if tabViewModel.canReload { + if tabViewModel.canFindInPage { addItem(withTitle: UserText.findInPageMenuItem, action: #selector(findInPage(_:)), keyEquivalent: "f") .targetting(self) .withImage(.findSearch) .withAccessibilityIdentifier("MoreOptionsMenu.findInPage") + } + if tabViewModel.canReload { addItem(withTitle: UserText.shareMenuItem, action: nil, keyEquivalent: "") .targetting(self) .withImage(.share) diff --git a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift index 3c24609a30..798e5f153f 100644 --- a/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift +++ b/DuckDuckGo/RecentlyClosed/View/RecentlyClosedMenu.swift @@ -71,10 +71,10 @@ private extension NSMenuItem { image = TabViewModel.Favicon.home title = UserText.tabHomeTitle case .settings: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.settings title = UserText.tabPreferencesTitle case .bookmarks: - image = TabViewModel.Favicon.preferences + image = TabViewModel.Favicon.bookmarks title = UserText.tabPreferencesTitle case .url, .subscription, .identityTheftRestoration: image = recentlyClosedTab.favicon diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index 99f9fcbf2c..2ec1087956 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -466,7 +466,10 @@ protocol NewWindowPolicyDecisionMaker { } else if content != self.content { self.content = content } - } else if self.content.isUrl { + } else if self.content.isUrl, + // DuckURLSchemeHandler redirects duck:// address to a simulated request + // ignore webView.url temporarily switching to `nil` + self.content.urlForWebView?.isDuckPlayer != true { // when e.g. opening a download in new tab - web view restores `nil` after the navigation is interrupted // maybe it worths adding another content type like .interruptedLoad(URL) to display a URL in the address bar self.content = .none diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift index ef1365ec9c..e9726a5dc0 100644 --- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift +++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift @@ -26,12 +26,14 @@ final class TabViewModel { enum Favicon { static let home = NSImage.homeFavicon + static let duckPlayer = NSImage.duckPlayerSettings static let burnerHome = NSImage.burnerTabFavicon - static let preferences = NSImage.preferences - static let bookmarks = NSImage.bookmarks - static let dataBrokerProtection = NSImage.dbpIcon - static let subscription = NSImage.subscriptionIcon - static let identityTheftRestoration = NSImage.itrIcon + static let settings = NSImage.settingsMulticolor16 + static let bookmarks = NSImage.bookmarksFolder + static let emailProtection = NSImage.emailProtectionIcon + static let dataBrokerProtection = NSImage.personalInformationRemovalMulticolor16 + static let subscription = NSImage.privacyPro + static let identityTheftRestoration = NSImage.identityTheftRestorationMulticolor16 } private(set) var tab: Tab @@ -62,7 +64,8 @@ final class TabViewModel { var loadingStartTime: CFTimeInterval? @Published private(set) var addressBarString: String = "" - @Published private(set) var passiveAddressBarString: String = "" + @Published private(set) var passiveAddressBarAttributedString = NSAttributedString() + var lastAddressBarTextFieldValue: AddressBarTextField.Value? @Published private(set) var title: String = UserText.tabHomeTitle @@ -80,6 +83,19 @@ final class TabViewModel { !isShowingErrorPage && canReload && !tab.webView.isInFullScreenMode } + var canFindInPage: Bool { + guard !isShowingErrorPage else { return false } + switch tab.content { + case .url(let url, _, _): + return !(url.isDuckPlayer || url.isDuckURLScheme) + case .subscription, .identityTheftRestoration: + return true + + case .newtab, .settings, .bookmarks, .onboarding, .dataBrokerProtection, .none: + return false + } + } + init(tab: Tab, appearancePreferences: AppearancePreferences = .shared, accessibilityPreferences: AccessibilityPreferences = .shared) { @@ -117,7 +133,7 @@ final class TabViewModel { case .url(let url, _, source: .webViewUpdated), .url(let url, _, source: .link): - guard !url.isEmpty, url != .blankPage else { fallthrough } + guard !url.isEmpty, url != .blankPage, !url.isDuckPlayer else { fallthrough } // Only display the Tab content URL update matching its Security Origin // see https://github.com/mozilla-mobile/firefox-ios/wiki/WKWebView-navigation-and-security-considerations @@ -215,9 +231,8 @@ final class TabViewModel { } private func subscribeToPreferences() { - appearancePreferences.$showFullURL.dropFirst().sink { [weak self] newValue in - guard let self = self, let url = self.tabURL, let host = self.tabHostURL else { return } - self.updatePassiveAddressBarString(showURL: newValue, url: url, hostURL: host) + appearancePreferences.$showFullURL.dropFirst().sink { [weak self] showFullURL in + self?.updatePassiveAddressBarString(showFullURL: showFullURL) }.store(in: &cancellables) accessibilityPreferences.$defaultPageZoom.sink { [weak self] newValue in guard let self = self else { return } @@ -236,56 +251,62 @@ final class TabViewModel { canBeBookmarked = !isShowingErrorPage && tab.content.canBeBookmarked } - private var tabURL: URL? { - return tab.content.userEditableUrl - } - - private var tabHostURL: URL? { - return tabURL?.root + private func updateAddressBarStrings() { + updateAddressBarString() + updatePassiveAddressBarString() } - private func updateAddressBarStrings() { - guard tab.content.isUrl, let url = tabURL else { - addressBarString = "" - passiveAddressBarString = "" - return - } + private func updateAddressBarString() { + addressBarString = { + guard ![.none, .onboarding, .newtab].contains(tab.content), + let url = tab.content.userEditableUrl else { return "" } - if url.isFileURL { - addressBarString = url.absoluteString - passiveAddressBarString = url.absoluteString - return - } + if url.isBlobURL { + return url.strippingUnsupportedCredentials() + } + return url.absoluteString + }() + } - if url.isDataURL { - addressBarString = url.absoluteString - passiveAddressBarString = "data:" - return + private func updatePassiveAddressBarString(showFullURL: Bool? = nil) { + let showFullURL = showFullURL ?? appearancePreferences.showFullURL + passiveAddressBarAttributedString = switch tab.content { + case .newtab, .onboarding, .none: + .init() // empty + case .settings: + .settingsTrustedIndicator + case .bookmarks: + .bookmarksTrustedIndicator + case .dataBrokerProtection: + .dbpTrustedIndicator + case .subscription: + .subscriptionTrustedIndicator + case .identityTheftRestoration: + .identityTheftRestorationTrustedIndicator + case .url(let url, _, _) where url.isDuckPlayer: + .duckPlayerTrustedIndicator + case .url(let url, _, _) where url.isEmailProtection: + .emailProtectionTrustedIndicator + case .url(let url, _, _): + NSAttributedString(string: passiveAddressBarString(with: url, showFullURL: showFullURL)) } + } + private func passiveAddressBarString(with url: URL, showFullURL: Bool) -> String { if url.isBlobURL { - let strippedUrl = url.stripUnsupportedCredentials() - addressBarString = strippedUrl - passiveAddressBarString = strippedUrl - return - } + url.strippingUnsupportedCredentials() - guard let hostURL = tabHostURL else { - // also lands here for about:blank and about:home - addressBarString = "" - passiveAddressBarString = "" - return - } + } else if url.isDataURL { + "data:" - addressBarString = url.absoluteString - updatePassiveAddressBarString(showURL: appearancePreferences.showFullURL, url: url, hostURL: hostURL) - } + } else if !showFullURL && url.isFileURL { + url.lastPathComponent - private func updatePassiveAddressBarString(showURL: Bool, url: URL, hostURL: URL) { - if showURL { - passiveAddressBarString = url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) - } else { - passiveAddressBarString = hostURL.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() + } else if !showFullURL && url.host?.isEmpty == false { + url.root?.toString(decodePunycode: true, dropScheme: true, dropTrailingSlash: true).droppingWwwPrefix() ?? "" + + } else /* display full url */ { + url.toString(decodePunycode: true, dropScheme: false, dropTrailingSlash: true) } } @@ -332,41 +353,33 @@ final class TabViewModel { } } + // swiftlint:disable:next cyclomatic_complexity private func updateFavicon(_ tabFavicon: NSImage?? = .none /* provided from .sink or taken from tab.favicon (optional) if .none */) { guard !isShowingErrorPage else { favicon = errorFaviconToShow(error: tab.error) return } - switch tab.content { + favicon = switch tab.content { case .dataBrokerProtection: - favicon = Favicon.dataBrokerProtection - return + Favicon.dataBrokerProtection + case .newtab where tab.burnerMode.isBurner: + Favicon.burnerHome case .newtab: - if tab.burnerMode.isBurner { - favicon = Favicon.burnerHome - } else { - favicon = Favicon.home - } - return + Favicon.home case .settings: - favicon = Favicon.preferences - return + Favicon.settings case .bookmarks: - favicon = Favicon.bookmarks - return + Favicon.bookmarks case .subscription: - favicon = Favicon.subscription - return + Favicon.subscription case .identityTheftRestoration: - favicon = Favicon.identityTheftRestoration - return - case .url, .onboarding, .none: break - } - - if let favicon: NSImage? = tabFavicon { - self.favicon = favicon - } else { - self.favicon = tab.favicon + Favicon.identityTheftRestoration + case .url(let url, _, _) where url.isDuckPlayer: + Favicon.duckPlayer + case .url(let url, _, _) where url.isEmailProtection: + Favicon.emailProtection + case .url, .onboarding, .none: + tabFavicon ?? tab.favicon } } @@ -426,3 +439,61 @@ extension TabViewModel: TabDataClearing { } } + +private extension NSAttributedString { + + private typealias Component = NSAttributedString + + private static let spacer = NSImage() // empty spacer image attachment for Attributed Strings below + + private static let iconBaselineOffset: CGFloat = -3 + private static let iconSize: CGFloat = 16 + private static let iconSpacing: CGFloat = 6 + private static let chevronSize: CGFloat = 12 + private static let chevronSpacing: CGFloat = 12 + + private static let duckDuckGoWithChevronAttributedString = NSAttributedString { + // logo + Component(image: .homeFavicon, rect: CGRect(x: 0, y: iconBaselineOffset, width: iconSize, height: iconSize)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // DuckDuckGo + Component(string: UserText.duckDuckGo) + + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + // chevron + Component(image: .chevronRight12, rect: CGRect(x: 0, y: -1, width: chevronSize, height: chevronSize)) + // spacing (wide) + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: chevronSpacing, height: 1)) + } + + private static func trustedIndicatorAttributedString(with icon: NSImage, title: String) -> NSAttributedString { + NSAttributedString { + duckDuckGoWithChevronAttributedString + + // favicon + Component(image: icon, rect: CGRect(x: 0, y: iconBaselineOffset, width: icon.size.width, height: icon.size.height)) + // spacing + Component(image: spacer, rect: CGRect(x: 0, y: 0, width: iconSpacing, height: 1)) + // title + Component(string: title) + } + } + + static let settingsTrustedIndicator = trustedIndicatorAttributedString(with: .settingsMulticolor16, + title: UserText.settings) + static let bookmarksTrustedIndicator = trustedIndicatorAttributedString(with: .bookmarksFolder, + title: UserText.bookmarks) + static let dbpTrustedIndicator = trustedIndicatorAttributedString(with: .personalInformationRemovalMulticolor16, + title: UserText.tabDataBrokerProtectionTitle) + static let subscriptionTrustedIndicator = trustedIndicatorAttributedString(with: .privacyPro, + title: UserText.subscription) + static let identityTheftRestorationTrustedIndicator = trustedIndicatorAttributedString(with: .identityTheftRestorationMulticolor16, + title: UserText.identityTheftRestorationOptionsMenuItem) + static let duckPlayerTrustedIndicator = trustedIndicatorAttributedString(with: .duckPlayerSettings, + title: UserText.duckPlayer) + static let emailProtectionTrustedIndicator = trustedIndicatorAttributedString(with: .emailProtectionIcon, + title: UserText.emailProtectionPreferences) + +} diff --git a/IntegrationTests/Tab/AddressBarTests.swift b/IntegrationTests/Tab/AddressBarTests.swift index 50d74ead32..e5297ed96b 100644 --- a/IntegrationTests/Tab/AddressBarTests.swift +++ b/IntegrationTests/Tab/AddressBarTests.swift @@ -215,7 +215,7 @@ class AddressBarTests: XCTestCase { XCTAssertEqual(addressBarValue, "", "\(idx)") } else { XCTAssertFalse(isAddressBarFirstResponder, "\(idx)") - XCTAssertEqual(addressBarValue, tab.content.isUrl ? tab.content.userEditableUrl!.absoluteString : "", "\(idx)") + XCTAssertEqual(addressBarValue, tab.content == .newtab ? "" : tab.content.userEditableUrl!.absoluteString, "\(idx)") } } } diff --git a/UnitTests/Tab/ViewModel/TabViewModelTests.swift b/UnitTests/Tab/ViewModel/TabViewModelTests.swift index 9dbaa4a0fd..d06378f00f 100644 --- a/UnitTests/Tab/ViewModel/TabViewModelTests.swift +++ b/UnitTests/Tab/ViewModel/TabViewModelTests.swift @@ -74,10 +74,32 @@ final class TabViewModelTests: XCTestCase { } @MainActor - func testWhenURLIsFileURLThenAddressBarIsFilePath() { + func testWhenURLIsFileURLAndShowFullUrlIsDisabledThenAddressBarIsFileName() { let urlString = "file:///Users/Dax/file.txt" let url = URL.makeURL(from: urlString)! - let tabViewModel = TabViewModel.forTabWithURL(url) + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: false)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) + + let addressBarStringExpectation = expectation(description: "Address bar string") + + tabViewModel.simulateLoadingCompletion(url, in: tabViewModel.tab.webView) + + tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in + XCTAssertEqual(tabViewModel.addressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, url.lastPathComponent) + addressBarStringExpectation.fulfill() + } .store(in: &cancellables) + waitForExpectations(timeout: 1, handler: nil) + } + + @MainActor + func testWhenURLIsFileURLAndShowFullUrlIsEnabledThenAddressBarIsFilePath() { + let urlString = "file:///Users/Dax/file.txt" + let url = URL.makeURL(from: urlString)! + let tab = Tab(content: .url(url, source: .link)) + let appearancePreferences = AppearancePreferences(persistor: AppearancePreferencesPersistorMock(showFullURL: true)) + let tabViewModel = TabViewModel(tab: tab, appearancePreferences: appearancePreferences) let addressBarStringExpectation = expectation(description: "Address bar string") @@ -85,7 +107,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, urlString) + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, urlString) addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil) @@ -103,7 +125,7 @@ final class TabViewModelTests: XCTestCase { tabViewModel.$addressBarString.debounce(for: 0.1, scheduler: RunLoop.main).sink { _ in XCTAssertEqual(tabViewModel.addressBarString, urlString) - XCTAssertEqual(tabViewModel.passiveAddressBarString, "data:") + XCTAssertEqual(tabViewModel.passiveAddressBarAttributedString.string, "data:") addressBarStringExpectation.fulfill() } .store(in: &cancellables) waitForExpectations(timeout: 1, handler: nil)