Skip to content

Commit

Permalink
Create TabBarRemoteMessagePresentable
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira committed Dec 17, 2024
1 parent 5e8c3b9 commit b3b001b
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 108 deletions.
6 changes: 6 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -4865,6 +4867,7 @@
BB5F46A22C8751F6005F72DF /* BookmarkSortTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSortTests.swift; sourceTree = "<group>"; };
BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireWindowTests.swift; sourceTree = "<group>"; };
BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSearchAndSortMetrics.swift; sourceTree = "<group>"; };
BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessagePresentable.swift; sourceTree = "<group>"; };
BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModelTests.swift; sourceTree = "<group>"; };
BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessage.swift; sourceTree = "<group>"; };
BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarActiveRemoteMessage.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -8339,6 +8342,7 @@
AA86491224D831A1001BABEE /* View */ = {
isa = PBXGroup;
children = (
BB7BA64B2D11FC150020B47E /* TabBarRemoteMessagePresentable.swift */,
AA80EC7B256C46AA007083E7 /* TabBar.storyboard */,
1430DFF424D0580F00B8978C /* TabBarViewController.swift */,
1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
135 changes: 135 additions & 0 deletions DuckDuckGo/TabBar/View/TabBarRemoteMessagePresentable.swift
Original file line number Diff line number Diff line change
@@ -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<TabBarRemoteMessageView>? { 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
}

}
115 changes: 8 additions & 107 deletions DuckDuckGo/TabBar/View/TabBarViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<TabBarRemoteMessageView>?

private var selectionIndexCancellable: AnyCancellable?
private var mouseDownCancellable: AnyCancellable?
private var cancellables = Set<AnyCancellable>()
private var tabBarRemoteMessageCancellable: AnyCancellable?

// TabBarRemoteMessagePresentable
var tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel
var tabBarRemoteMessagePopover: NSPopover?
var tabBarRemoteMessagePopoverHoverTimer: Timer?
var feedbackBarButtonHostingController: NSHostingController<TabBarRemoteMessageView>?
var tabBarRemoteMessageCancellable: AnyCancellable?

@IBOutlet weak var shadowView: TabShadowView!

Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit b3b001b

Please sign in to comment.