diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index eb09ffd6dd..2d951916ac 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2882,6 +2882,8 @@ BB731F312CDBA6360023D2E4 /* FireWindowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */; }; BB7B5F982C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; + BB7BA64C2D11FC170020B47E /* TabBarRemoteMessagePresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresentable.swift */; }; + BB7BA64D2D11FC170020B47E /* TabBarRemoteMessagePresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresentable.swift */; }; BB9BA2202D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */; }; BB9BA2212D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */; }; BB9BA2262D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */; }; @@ -4865,6 +4867,7 @@ BB5F46A22C8751F6005F72DF /* BookmarkSortTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSortTests.swift; sourceTree = ""; }; BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireWindowTests.swift; sourceTree = ""; }; BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSearchAndSortMetrics.swift; sourceTree = ""; }; + BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessagePresentable.swift; sourceTree = ""; }; BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModelTests.swift; sourceTree = ""; }; BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessage.swift; sourceTree = ""; }; BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarActiveRemoteMessage.swift; sourceTree = ""; }; @@ -8339,6 +8342,7 @@ AA86491224D831A1001BABEE /* View */ = { isa = PBXGroup; children = ( + BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresentable.swift */, AA80EC7B256C46AA007083E7 /* TabBar.storyboard */, 1430DFF424D0580F00B8978C /* TabBarViewController.swift */, 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */, @@ -11355,6 +11359,7 @@ 7BFF35742C10D75000F89673 /* IPCServiceLauncher.swift in Sources */, F1D042A22BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 370270BD2C78B6D3002E44E4 /* NewTabBackgroundPixel.swift in Sources */, + BB7BA64C2D11FC170020B47E /* TabBarRemoteMessagePresentable.swift in Sources */, 3706FAA3293F65D500E42796 /* CrashReportPromptViewController.swift in Sources */, BD88A83F2C4F3E4300460A26 /* FeedbackCategoryProviding.swift in Sources */, 3706FAA4293F65D500E42796 /* ContextMenuManager.swift in Sources */, @@ -13162,6 +13167,7 @@ 315AA07028CA5CC800200030 /* YoutubePlayerNavigationHandler.swift in Sources */, 56DB9FE92CD24B47001BEC23 /* ContextualOnboardingPixel.swift in Sources */, 37AFCE9227DB8CAD00471A10 /* PreferencesAboutView.swift in Sources */, + BB7BA64D2D11FC170020B47E /* TabBarRemoteMessagePresentable.swift in Sources */, F1D042A12BFBB4DD00A31506 /* DataBrokerProtectionSettings+Environment.swift in Sources */, 4B2F565C2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */, 9826B0A02747DF3D0092F683 /* ContentBlocking.swift in Sources */, diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 3289912570..265658a8a0 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,7 +47,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! #endif diff --git a/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresentable.swift b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresentable.swift new file mode 100644 index 0000000000..69382d5fd7 --- /dev/null +++ b/DuckDuckGo/TabBar/View/TabBarRemoteMessagePresentable.swift @@ -0,0 +1,135 @@ +// +// TabBarRemoteMessagePresentable.swift +// +// 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 SwiftUI +import Combine + +protocol TabBarRemoteMessagePresentable: AnyObject { + var tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel { get } + var rightSideStackView: NSStackView! { get } + var tabBarRemoteMessagePopover: NSPopover? { get set } + var tabBarRemoteMessagePopoverHoverTimer: Timer? { get set } + var feedbackBarButtonHostingController: NSHostingController? { get set } + var tabBarRemoteMessageCancellable: AnyCancellable? { get set } +} + +extension TabBarRemoteMessagePresentable { + + func addTabBarRemoteMessageListener() { + tabBarRemoteMessageCancellable = tabBarRemoteMessageViewModel.$remoteMessage + .sink(receiveValue: { tabBarRemoteMessage in + if let tabBarRemoteMessage = tabBarRemoteMessage { + if self.feedbackBarButtonHostingController == nil { + self.showTabBarRemoteMessage(tabBarRemoteMessage) + } + } else { + if self.feedbackBarButtonHostingController != nil { + self.removeFeedbackButton() + } + } + }) + } + + private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { + let feedbackButtonView = TabBarRemoteMessageView( + model: tabBarRemotMessage, + onClose: { + self.tabBarRemoteMessageViewModel.onMessageDismissed() + self.removeFeedbackButton() + }, + onTap: { surveyURL in + DispatchQueue.main.async { + WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) + } + self.tabBarRemoteMessageViewModel.onSurveyOpened() + self.removeFeedbackButton() + }, + onHover: { + self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) + }, + onHoverEnd: { + self.dismissTabBarRemoteMessagePopover() + }, + onAppear: { + self.tabBarRemoteMessageViewModel.markTabBarRemoteMessageAsShown() + } + ) + feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) + guard let feedbackBarButtonHostingController else { return } + + feedbackBarButtonHostingController.view.translatesAutoresizingMaskIntoConstraints = false + + // Insert the hosting controller's view into the stack view just before the fire button + let index = max(0, rightSideStackView.arrangedSubviews.count - 1) + rightSideStackView.insertArrangedSubview(feedbackBarButtonHostingController.view, at: index) + + NSLayoutConstraint.activate([ + feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) + ]) + } + + private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in + self.showTabBarRemotePopup(message) + } + } + + private func dismissTabBarRemoteMessagePopover() { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopover?.close() + } + + private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + + if let popover = tabBarRemoteMessagePopover { + popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } else { + tabBarRemoteMessagePopover = NSPopover() + configurePopover(with: message) + + tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } + } + + private func configurePopover(with message: TabBarRemoteMessage) { + guard let popover = tabBarRemoteMessagePopover else { return } + + popover.animates = true + popover.behavior = .semitransient + popover.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, + height: TabBarRemoteMessagePopoverContent.Constants.height) + + let controller = NSViewController() + controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + popover.contentViewController = controller + } + + private func removeFeedbackButton() { + guard let hostingController = feedbackBarButtonHostingController else { return } + + rightSideStackView.removeArrangedSubview(hostingController.view) + hostingController.view.removeFromSuperview() + hostingController.removeFromParent() + feedbackBarButtonHostingController = nil + } + +} diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 1e3191125a..c9d82d8375 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -25,7 +25,7 @@ import WebKit import os.log import RemoteMessaging -final class TabBarViewController: NSViewController { +final class TabBarViewController: NSViewController, TabBarRemoteMessagePresentable { enum HorizontalSpace: CGFloat { case pinnedTabsScrollViewPadding = 76 @@ -71,15 +71,16 @@ final class TabBarViewController: NSViewController { private let pinnedTabsViewModel: PinnedTabsViewModel? private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? - private let tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel - private var tabBarRemoteMessagePopover: NSPopover? - private var tabBarRemoteMessagePopoverHoverTimer: Timer? - private var feedbackBarButtonHostingController: NSHostingController? - private var selectionIndexCancellable: AnyCancellable? private var mouseDownCancellable: AnyCancellable? private var cancellables = Set() - private var tabBarRemoteMessageCancellable: AnyCancellable? + + // TabBarRemoteMessagePresentable + var tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel + var tabBarRemoteMessagePopover: NSPopover? + var tabBarRemoteMessagePopoverHoverTimer: Timer? + var feedbackBarButtonHostingController: NSHostingController? + var tabBarRemoteMessageCancellable: AnyCancellable? @IBOutlet weak var shadowView: TabShadowView! @@ -211,106 +212,6 @@ final class TabBarViewController: NSViewController { } } - private func addTabBarRemoteMessageListener() { - tabBarRemoteMessageCancellable = tabBarRemoteMessageViewModel.$remoteMessage - .sink(receiveValue: { tabBarRemoteMessage in - if let tabBarRemoteMessage = tabBarRemoteMessage { - if self.feedbackBarButtonHostingController == nil { - self.showTabBarRemoteMessage(tabBarRemoteMessage) - } - } else { - if self.feedbackBarButtonHostingController != nil { - self.removeFeedbackButton() - } - } - }) - } - - private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { - let feedbackButtonView = TabBarRemoteMessageView( - model: tabBarRemotMessage, - onClose: { - self.tabBarRemoteMessageViewModel.onMessageDismissed() - self.removeFeedbackButton() - }, - onTap: { surveyURL in - WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) - self.tabBarRemoteMessageViewModel.onSurveyOpened() - self.removeFeedbackButton() - }, - onHover: { - self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) - }, - onHoverEnd: { - self.dismissTabBarRemoteMessagePopover() - }, - onAppear: { - self.tabBarRemoteMessageViewModel.markTabBarRemoteMessageAsShown() - } - ) - feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) - guard let feedbackBarButtonHostingController else { return } - - feedbackBarButtonHostingController.view.translatesAutoresizingMaskIntoConstraints = false - - // Insert the hosting controller's view into the stack view just before the fire button - let index = max(0, rightSideStackView.arrangedSubviews.count - 1) - rightSideStackView.insertArrangedSubview(feedbackBarButtonHostingController.view, at: index) - - NSLayoutConstraint.activate([ - feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) - ]) - } - - private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { - tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in - self.showTabBarRemotePopup(message) - } - } - - private func dismissTabBarRemoteMessagePopover() { - tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopover?.close() - } - - private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { - guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { - return - } - - if let popover = tabBarRemoteMessagePopover { - popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) - } else { - tabBarRemoteMessagePopover = NSPopover() - configurePopover(with: message) - - tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) - } - } - - private func configurePopover(with message: TabBarRemoteMessage) { - guard let popover = tabBarRemoteMessagePopover else { return } - - popover.animates = true - popover.behavior = .semitransient - popover.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, - height: TabBarRemoteMessagePopoverContent.Constants.height) - - let controller = NSViewController() - controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) - popover.contentViewController = controller - } - - private func removeFeedbackButton() { - guard let hostingController = feedbackBarButtonHostingController else { return } - - rightSideStackView.removeArrangedSubview(hostingController.view) - hostingController.view.removeFromSuperview() - hostingController.removeFromParent() - feedbackBarButtonHostingController = nil - } - private func setupPinnedTabsView() { layoutPinnedTabsView() subscribeToPinnedTabsViewModelOutputs()