From 521431847ee842bc61638d4b28844520d6260d41 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 8 Nov 2024 15:15:27 +0000 Subject: [PATCH] WIP POC --- DuckDuckGo.xcodeproj/project.pbxproj | 20 ++++- DuckDuckGo/AIChat/AIChatViewController.swift | 77 +++++++++++++++++++ DuckDuckGo/MainViewController.swift | 18 +++++ DuckDuckGo/TabDelegate.swift | 3 + ...bViewControllerBrowsingMenuExtension.swift | 41 +++++----- 5 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 DuckDuckGo/AIChat/AIChatViewController.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index f77ca62941..609d4e9e38 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -168,6 +168,7 @@ 316931D927BD22A80095F5ED /* DownloadActionMessageViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316931D827BD22A80095F5ED /* DownloadActionMessageViewHelper.swift */; }; 3170048227A9504F00C03F35 /* DownloadMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3170048127A9504F00C03F35 /* DownloadMocks.swift */; }; 317045C02858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */; }; + 317F2FBB2CDE59850019592C /* AIChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F2FBA2CDE597B0019592C /* AIChatViewController.swift */; }; 317F5F982C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */; }; 31860A5B2C57ED2D005561F5 /* DuckPlayerStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */; }; 31951E8E2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */; }; @@ -1481,6 +1482,7 @@ 3170048127A9504F00C03F35 /* DownloadMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadMocks.swift; sourceTree = ""; }; 317045BF2858C6B90016ED1F /* AutofillInterfaceEmailTruncatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillInterfaceEmailTruncatorTests.swift; sourceTree = ""; }; 31794BFF2821DFB600F18633 /* DuckUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = DuckUI; sourceTree = ""; }; + 317F2FBA2CDE597B0019592C /* AIChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIChatViewController.swift; sourceTree = ""; }; 317F5F972C94A9EB0081666F /* MarketplaceAdPostbackStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketplaceAdPostbackStorage.swift; sourceTree = ""; }; 31860A5A2C57ED2D005561F5 /* DuckPlayerStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DuckPlayerStorage.swift; sourceTree = ""; }; 31951E8D2823003200CAF535 /* AutofillLoginDetailsHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginDetailsHeaderView.swift; sourceTree = ""; }; @@ -1640,9 +1642,9 @@ 6FBF0F8A2BD7C0A900136CF0 /* AllProtectedCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllProtectedCell.swift; sourceTree = ""; }; 6FD0C41E2C5BF097000561C9 /* NewTabPageIntroMessageSetupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageIntroMessageSetupTests.swift; sourceTree = ""; }; 6FD0C4202C5BF774000561C9 /* NewTabPageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTabPageViewModelTests.swift; sourceTree = ""; }; - 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = AdAttribution/AdAttributionPixelReporter.swift; sourceTree = ""; }; - 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = AdAttribution/AdAttributionReporterStorage.swift; sourceTree = ""; }; - 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; + 6FD1BAE12B87A107000C475C /* AdAttributionPixelReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionPixelReporter.swift; path = ../AdAttribution/AdAttributionPixelReporter.swift; sourceTree = ""; }; + 6FD1BAE22B87A107000C475C /* AdAttributionReporterStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionReporterStorage.swift; path = ../AdAttribution/AdAttributionReporterStorage.swift; sourceTree = ""; }; + 6FD1BAE32B87A107000C475C /* AdAttributionFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AdAttributionFetcher.swift; path = ../AdAttribution/AdAttributionFetcher.swift; sourceTree = ""; }; 6FD3AEE12B8DFBB80060FCCC /* AdAttributionPixelReporterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdAttributionPixelReporterTests.swift; sourceTree = ""; }; 6FD3F80E2C3EF4F000DA5797 /* DeviceOrientationEnvironmentValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceOrientationEnvironmentValue.swift; sourceTree = ""; }; 6FD3F8122C3EFDA200DA5797 /* FavoritesPreviewDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoritesPreviewDataSource.swift; sourceTree = ""; }; @@ -3581,6 +3583,15 @@ name = Utils; sourceTree = ""; }; + 317F2FB92CDE59720019592C /* AIChat */ = { + isa = PBXGroup; + children = ( + 317F2FBA2CDE597B0019592C /* AIChatViewController.swift */, + 6FD1BAE02B87A0E8000C475C /* AdAttribution */, + ); + path = AIChat; + sourceTree = ""; + }; 31951E9328230D8900CAF535 /* Shared */ = { isa = PBXGroup; children = ( @@ -4213,7 +4224,7 @@ 84E341941E2F7EFB00BDBA6F /* DuckDuckGo */ = { isa = PBXGroup; children = ( - 6FD1BAE02B87A0E8000C475C /* AdAttribution */, + 317F2FB92CDE59720019592C /* AIChat */, AA4D6A8023DE4973007E8790 /* AppIcon */, F1C5ECF31E37812900C599A4 /* Application */, 9817C9C121EF58BA00884F65 /* AutoClear */, @@ -7809,6 +7820,7 @@ F1C4A70E1E57725800A6CA1B /* OmniBar.swift in Sources */, 981CA7EA2617797500E119D5 /* MainViewController+AddFavoriteFlow.swift in Sources */, 373608902ABB1E6C00629E7F /* FavoritesDisplayModeStorage.swift in Sources */, + 317F2FBB2CDE59850019592C /* AIChatViewController.swift in Sources */, 9872D205247DCAC100CEF398 /* TabPreviewsSource.swift in Sources */, 4B0F3F502B9BFF2100392892 /* NetworkProtectionFAQView.swift in Sources */, F130D73A1E5776C500C45811 /* OmniBarDelegate.swift in Sources */, diff --git a/DuckDuckGo/AIChat/AIChatViewController.swift b/DuckDuckGo/AIChat/AIChatViewController.swift new file mode 100644 index 0000000000..a9dc2ee2f6 --- /dev/null +++ b/DuckDuckGo/AIChat/AIChatViewController.swift @@ -0,0 +1,77 @@ +// +// AIChatViewController.swift +// DuckDuckGo +// +// Copyright © 2024 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 UIKit +import WebKit + +final class AIChatViewController: UIViewController { + private let url: URL + private var webView: WKWebView! + private var closeButton: UIButton! + + init(url: URL) { + self.url = url + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + webView = WKWebView(frame: .zero) + + view.addSubview(webView) + + webView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.topAnchor.constraint(equalTo: view.topAnchor), + webView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + let request = URLRequest(url: url) + webView.load(request) + + closeButton = UIButton(type: .system) + closeButton.setTitle("X", for: .normal) + closeButton.setTitleColor(.white, for: .normal) + closeButton.backgroundColor = UIColor.black.withAlphaComponent(0.5) + closeButton.layer.cornerRadius = 15 + closeButton.clipsToBounds = true + closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside) + + view.addSubview(closeButton) + + closeButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + closeButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16), + closeButton.widthAnchor.constraint(equalToConstant: 30), + closeButton.heightAnchor.constraint(equalToConstant: 30) + ]) + } + + @objc private func closeButtonTapped() { + dismiss(animated: true, completion: nil) + } +} diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 9726c00630..1b057eb512 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -78,6 +78,9 @@ class MainViewController: UIViewController { return emailManager }() + private lazy var aiChatViewController = AIChatViewController(url: URL(string: "http://duck.ai")!) + private lazy var gptChatViewController = AIChatViewController(url: URL(string: "http://chatgpt.com")!) + var newTabPageViewController: NewTabPageViewController? var tabsBarController: TabsBarViewController? var suggestionTrayController: SuggestionTrayViewController? @@ -2296,6 +2299,21 @@ extension MainViewController: TabDelegate { segueToReportBrokenSite() } + func tabDidRequestAIChatFullScreen(tab: TabViewController) { + aiChatViewController.modalPresentationStyle = .fullScreen + tab.present(aiChatViewController, animated: true, completion: nil) + } + + func tabDidRequestAIChatModal(tab: TabViewController) { + aiChatViewController.modalPresentationStyle = .pageSheet + tab.present(aiChatViewController, animated: true, completion: nil) + } + + func tabDidRequestGPT(tab: TabViewController) { + gptChatViewController.modalPresentationStyle = .fullScreen + tab.present(gptChatViewController, animated: true, completion: nil) + } + func tab(_ tab: TabViewController, didRequestToggleReportWithCompletionHandler completionHandler: @escaping (Bool) -> Void) { segueToReportBrokenSite(entryPoint: .toggleReport(completionHandler: completionHandler)) } diff --git a/DuckDuckGo/TabDelegate.swift b/DuckDuckGo/TabDelegate.swift index 90424e25fb..fb8e25ce7c 100644 --- a/DuckDuckGo/TabDelegate.swift +++ b/DuckDuckGo/TabDelegate.swift @@ -50,6 +50,9 @@ protocol TabDelegate: AnyObject { func tab(_ tab: TabViewController, didChangePrivacyInfo privacyInfo: PrivacyInfo?) func tabDidRequestReportBrokenSite(tab: TabViewController) + func tabDidRequestAIChatFullScreen(tab: TabViewController) + func tabDidRequestAIChatModal(tab: TabViewController) + func tabDidRequestGPT(tab: TabViewController) func tab(_ tab: TabViewController, didRequestToggleReportWithCompletionHandler completionHandler: @escaping (Bool) -> Void) diff --git a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift index 8e8d715052..b5d93129d9 100644 --- a/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift +++ b/DuckDuckGo/TabViewControllerBrowsingMenuExtension.swift @@ -39,30 +39,17 @@ extension TabViewController { self?.onNewTabAction() })) - entries.append(BrowsingMenuEntry.regular(name: UserText.actionShare, image: UIImage(named: "Share-24")!, action: { [weak self] in - guard let self = self else { return } - guard let menu = self.chromeDelegate?.omniBar.menuButton else { return } - Pixel.fire(pixel: .browsingMenuShare) - self.onShareAction(forLink: self.link!, fromView: menu) - })) + entries.append(BrowsingMenuEntry.regular(name: "GPT", image: UIImage(named: "Share-24")!, action: { [weak self] in + self?.openGPT() - entries.append(BrowsingMenuEntry.regular(name: UserText.actionCopy, image: UIImage(named: "Copy-24")!, action: { [weak self] in - guard let strongSelf = self else { return } - if !strongSelf.isError, let url = strongSelf.webView.url { - strongSelf.onCopyAction(forUrl: url) - } else if let text = self?.chromeDelegate?.omniBar.textField.text { - strongSelf.onCopyAction(for: text) - } + })) - Pixel.fire(pixel: .browsingMenuCopy) - let addressBarBottom = strongSelf.appSettings.currentAddressBarPosition.isBottom - ActionMessageView.present(message: UserText.actionCopyMessage, - presentationLocation: .withBottomBar(andAddressBarBottom: addressBarBottom)) + entries.append(BrowsingMenuEntry.regular(name: "Modal", image: UIImage(named: "Copy-24")!, action: { [weak self] in + self?.openAIChatTabModal() })) - entries.append(BrowsingMenuEntry.regular(name: UserText.actionPrint, image: UIImage(named: "Print-24")!, action: { [weak self] in - Pixel.fire(pixel: .browsingMenuPrint) - self?.print() + entries.append(BrowsingMenuEntry.regular(name: "Full", image: UIImage(named: "Print-24")!, action: { [weak self] in + self?.openAIChatTabFullScreen() })) return entries @@ -166,7 +153,19 @@ extension TabViewController { return entries } - + + private func openAIChatTabFullScreen() { + delegate?.tabDidRequestAIChatFullScreen(tab: self) + } + + private func openAIChatTabModal() { + delegate?.tabDidRequestAIChatModal(tab: self) + } + + private func openGPT() { + delegate?.tabDidRequestGPT(tab: self) + } + private func buildKeepSignInEntry(forLink link: Link) -> BrowsingMenuEntry? { guard let domain = link.url.host, !link.url.isDuckDuckGo else { return nil } let isFireproofed = PreserveLogins.shared.isAllowed(cookieDomain: domain)