From 270926458dddd1dd20c7bd39e3043262ee23ab31 Mon Sep 17 00:00:00 2001 From: Jacob Sikorski Date: Fri, 7 Jul 2023 14:47:29 +0200 Subject: [PATCH] Fix #6179: Add clean copy feature --- .../Extensions/UIAction+Extensions.swift | 52 ++++++++++ ...rowserViewController+ToolbarDelegate.swift | 97 +++++++++---------- ...rViewController+WKNavigationDelegate.swift | 13 ++- .../Brave/WebFilters/CleanURLService.swift | 27 ++++++ Sources/BraveStrings/BraveStrings.swift | 9 +- 5 files changed, 140 insertions(+), 58 deletions(-) create mode 100644 Sources/Brave/Extensions/UIAction+Extensions.swift create mode 100644 Sources/Brave/WebFilters/CleanURLService.swift diff --git a/Sources/Brave/Extensions/UIAction+Extensions.swift b/Sources/Brave/Extensions/UIAction+Extensions.swift new file mode 100644 index 00000000000..f58ee9eb3c8 --- /dev/null +++ b/Sources/Brave/Extensions/UIAction+Extensions.swift @@ -0,0 +1,52 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import UIKit +import Strings + +extension UIAction { + static func makePasteAndGoAction(pasteCallback: @escaping (String) -> Void) -> UIAction { + return UIAction( + identifier: .pasteAndGo, + handler: UIAction.deferredActionHandler { _ in + if let pasteboardContents = UIPasteboard.general.string { + pasteCallback(pasteboardContents) + } + } + ) + } + + static func makePasteAction(pasteCallback: @escaping (String) -> Void) -> UIAction { + return UIAction( + identifier: .paste, + handler: UIAction.deferredActionHandler { _ in + if let pasteboardContents = UIPasteboard.general.string { + pasteCallback(pasteboardContents) + } + } + ) + } + + static func makeCopyAction(for url: URL) -> UIAction { + return UIAction( + title: Strings.copyLinkActionTitle, + image: UIImage(systemName: "doc.on.doc"), + handler: UIAction.deferredActionHandler { _ in + UIPasteboard.general.url = url as URL + } + ) + } + + static func makeCleanCopyAction(for url: URL, isPrivateMode: Bool) -> UIAction { + return UIAction( + title: Strings.copyCleanLink, + image: UIImage(systemName: "doc.on.doc"), + handler: UIAction.deferredActionHandler { _ in + let cleanedURL = CleanURLService.shared.cleanup(url: url, isPrivateMode: isPrivateMode) + UIPasteboard.general.url = cleanedURL + } + ) + } +} diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift index 3860e0c24c4..00f70c59dab 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+ToolbarDelegate.swift @@ -913,13 +913,17 @@ extension BrowserViewController: ToolbarDelegate { extension BrowserViewController: UIContextMenuInteractionDelegate { public func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { - - let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [unowned self] _ in - var actionMenus: [UIMenu?] = [] - var pasteMenuChildren: [UIAction] = [] - + let configuration = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [unowned self] _ in + var actionMenu: [UIMenu] = [] let tab = tabManager.selectedTab - var reloadMenu: UIMenu? + + if let pasteMenu = makePasteMenu() { + actionMenu.append(pasteMenu) + } + + if let copyMenu = makeCopyMenu(isPrivateMode: tab?.isPrivate ?? true) { + actionMenu.append(copyMenu) + } if let url = tab?.url, url.isWebPage() { let reloadTitle = tab?.isDesktopSite == true ? Strings.appMenuViewMobileSiteTitleString : Strings.appMenuViewDesktopSiteTitleString @@ -931,52 +935,11 @@ extension BrowserViewController: UIContextMenuInteractionDelegate { tab?.switchUserAgent() }) - reloadMenu = UIMenu(options: .displayInline, children: [reloadAction]) - } - - let pasteGoAction = UIAction( - identifier: .pasteAndGo, - handler: UIAction.deferredActionHandler { _ in - if let pasteboardContents = UIPasteboard.general.string { - self.topToolbar(self.topToolbar, didSubmitText: pasteboardContents) - } - }) - - let pasteAction = UIAction( - identifier: .paste, - handler: UIAction.deferredActionHandler { _ in - if let pasteboardContents = UIPasteboard.general.string { - self.topToolbar.enterOverlayMode(pasteboardContents, pasted: true, search: true) - } - }) - - pasteMenuChildren = [pasteGoAction, pasteAction] - - if #unavailable(iOS 16.0), isUsingBottomBar { - pasteMenuChildren.reverse() - } - - let copyAction = UIAction( - title: Strings.copyAddressTitle, - image: UIImage(systemName: "doc.on.doc"), - handler: UIAction.deferredActionHandler { _ in - if let url = self.topToolbar.currentURL { - UIPasteboard.general.url = url as URL - } - }) - - let copyMenu = UIMenu(options: .displayInline, children: [copyAction]) - - if UIPasteboard.general.hasStrings || UIPasteboard.general.hasURLs { - let pasteMenu = UIMenu(options: .displayInline, children: pasteMenuChildren) - actionMenus.append(contentsOf: [pasteMenu, copyMenu]) - } else { - actionMenus.append(copyMenu) + let reloadMenu = UIMenu(options: .displayInline, children: [reloadAction]) + actionMenu.append(reloadMenu) } - actionMenus.append(reloadMenu) - - return UIMenu(children: actionMenus.compactMap { $0 }) + return UIMenu(children: actionMenu) } if #available(iOS 16.0, *) { @@ -985,4 +948,38 @@ extension BrowserViewController: UIContextMenuInteractionDelegate { return configuration } + + private func makePasteMenu() -> UIMenu? { + guard UIPasteboard.general.hasStrings || UIPasteboard.general.hasURLs else { return nil } + + var children: [UIAction] = [ + UIAction.makePasteAndGoAction(pasteCallback: { pasteboardContents in + self.topToolbar(self.topToolbar, didSubmitText: pasteboardContents) + }), + UIAction.makePasteAction(pasteCallback: { pasteboardContents in + self.topToolbar.enterOverlayMode(pasteboardContents, pasted: true, search: true) + }) + ] + + if #unavailable(iOS 16.0), isUsingBottomBar { + children.reverse() + } + + return UIMenu(options: .displayInline, children: children) + } + + private func makeCopyMenu(isPrivateMode: Bool) -> UIMenu? { + guard let url = self.topToolbar.currentURL else { return nil } + + var children: [UIAction] = [ + UIAction.makeCopyAction(for: url), + UIAction.makeCleanCopyAction(for: url, isPrivateMode: isPrivateMode) + ] + + if #unavailable(iOS 16.0), isUsingBottomBar { + children.reverse() + } + + return UIMenu(options: .displayInline, children: children) + } } diff --git a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift index 4105380ecf8..727dddec0c0 100644 --- a/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift +++ b/Sources/Brave/Frontend/Browser/BrowserViewController/BrowserViewController+WKNavigationDelegate.swift @@ -984,6 +984,7 @@ extension BrowserViewController: WKUIDelegate { } } openNewPrivateTabAction.accessibilityLabel = "linkContextMenu.openInNewPrivateTab" + actions.append(openNewPrivateTabAction) if UIApplication.shared.supportsMultipleScenes { @@ -1018,15 +1019,13 @@ extension BrowserViewController: WKUIDelegate { actions.append(openNewPrivateWindowAction) } - let copyAction = UIAction( - title: Strings.copyLinkActionTitle, - image: UIImage(systemName: "doc.on.doc") - ) { _ in - UIPasteboard.general.url = url - } + let copyAction = UIAction.makeCopyAction(for: url) copyAction.accessibilityLabel = "linkContextMenu.copyLink" - actions.append(copyAction) + + let copyCleanLinkAction = UIAction.makeCleanCopyAction(for: url, isPrivateMode: currentTab.isPrivate) + copyCleanLinkAction.accessibilityLabel = "linkContextMenu.copyCleanLink" + actions.append(copyCleanLinkAction) if let braveWebView = webView as? BraveWebView { let shareAction = UIAction( diff --git a/Sources/Brave/WebFilters/CleanURLService.swift b/Sources/Brave/WebFilters/CleanURLService.swift new file mode 100644 index 00000000000..2b6318a5c9b --- /dev/null +++ b/Sources/Brave/WebFilters/CleanURLService.swift @@ -0,0 +1,27 @@ +// Copyright 2023 The Brave Authors. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import Foundation +import BraveCore + +/// A helper class that helps us clean urls for "clean copy" feature +class CleanURLService { + public static let shared = CleanURLService() + + private lazy var urlSanitizerService = URLSanitizerServiceFactory.get(privateMode: false) + private lazy var privateURLSanitizerService = URLSanitizerServiceFactory.get(privateMode: true) + + /// Initialize this instance with a network manager + init() {} + + /// Cleanup the url using the stored matcher + func cleanup(url: URL, isPrivateMode: Bool) -> URL { + if isPrivateMode { + return privateURLSanitizerService?.sanitizeURL(url) ?? url + } else { + return urlSanitizerService?.sanitizeURL(url) ?? url + } + } +} diff --git a/Sources/BraveStrings/BraveStrings.swift b/Sources/BraveStrings/BraveStrings.swift index 2bdaeef60b7..8d7709c5d38 100644 --- a/Sources/BraveStrings/BraveStrings.swift +++ b/Sources/BraveStrings/BraveStrings.swift @@ -33,7 +33,6 @@ extension Strings { public static let download = NSLocalizedString("CommonDownload", tableName: "BraveShared", bundle: .module, value: "Download", comment: "Text to choose for downloading a file (example: saving an image to phone)") public static let showLinkPreviewsActionTitle = NSLocalizedString("ShowLinkPreviewsActionTitle", tableName: "BraveShared", bundle: .module, value: "Show Link Previews", comment: "Context menu item for showing link previews") public static let hideLinkPreviewsActionTitle = NSLocalizedString("HideLinkPreviewsActionTitle", tableName: "BraveShared", bundle: .module, value: "Hide Link Previews", comment: "Context menu item for hiding link previews") - public static let copyAddressTitle = NSLocalizedString("CopyAddressTitle", bundle: .module, value: "Copy Address", comment: "The title for the button that lets you copy the url from the location bar.") public static let learnMore = NSLocalizedString( "learnMore", tableName: "BraveShared", bundle: .module, value: "Learn More", comment: "") @@ -974,6 +973,14 @@ extension Strings { public static let codeWordInputHelp = NSLocalizedString("CodeWordInputHelp", tableName: "BraveShared", bundle: .module, value: "Type your supplied sync chain code words into the form below.", comment: "Code words input help") public static let copyToClipboard = NSLocalizedString("CopyToClipboard", tableName: "BraveShared", bundle: .module, value: "Copy to Clipboard", comment: "Copy codewords title") public static let copiedToClipboard = NSLocalizedString("CopiedToClipboard", tableName: "BraveShared", bundle: .module, value: "Copied to Clipboard!", comment: "Copied codewords title") + + /// A menu option available when long pressing on a link which allows you to copy a clean version of the url which strips out some query parameters. + public static let copyCleanLink = NSLocalizedString( + "CopyCleanLink", tableName: "BraveShared", bundle: .module, + value: "Copy Clean Link", + comment: "A menu option available when long pressing on a link which allows you to copy a clean version of the url which strips out some query parameters." + ) + public static let syncUnsuccessful = NSLocalizedString("SyncUnsuccessful", tableName: "BraveShared", bundle: .module, value: "Unsuccessful", comment: "") public static let syncUnableCreateGroup = NSLocalizedString("SyncUnableCreateGroup", tableName: "BraveShared", bundle: .module, value: "Can't sync this device", comment: "Description on popup when setting up a sync group fails") public static let copied = NSLocalizedString("Copied", tableName: "BraveShared", bundle: .module, value: "Copied!", comment: "Copied action complete title")