From e8792304cf3d6b0123b594349920b4e4dbc93f4a Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Wed, 27 Nov 2024 17:37:32 -0300 Subject: [PATCH 1/9] Implement tab bar remote message --- DuckDuckGo.xcodeproj/project.pbxproj | 20 +++ .../dax-response.imageset/Contents.json | 12 ++ .../Response-DDG-Question-96x96.svg | 20 +++ .../Common/Extensions/URLExtension.swift | 4 + DuckDuckGo/HomePage/View/HomePageView.swift | 5 +- DuckDuckGo/Localizable.xcstrings | 2 +- .../MainWindow/MainViewController.swift | 2 +- .../ActiveRemoteMessageModel+NewTabPage.swift | 5 +- .../ActiveRemoteMessageModel.swift | 7 + .../RemoteMessagingClient.swift | 2 +- .../TabBarRemoteMessageView.swift | 163 ++++++++++++++++++ .../TabBarRemoteMessageViewModel.swift | 98 +++++++++++ .../TabBar/View/TabBarViewController.swift | 76 +++++++- 13 files changed, 408 insertions(+), 8 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg create mode 100644 DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift create mode 100644 DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 78b0fc71b0..1daa63df7b 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2853,6 +2853,8 @@ B6FA893F269C424500588ECD /* PrivacyDashboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */; }; B6FA8941269C425400588ECD /* PrivacyDashboardPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */; }; BB0346F52CEB80B400D23E05 /* DownloadsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB0346F42CEB80B400D23E05 /* DownloadsTests.swift */; }; + BB3229052D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */; }; + BB3229062D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */; }; BB4339DB2C7F9606005D7ED7 /* PinnedTabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4339DA2C7F9606005D7ED7 /* PinnedTabsTests.swift */; }; BB470EBB2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */; }; BB470EBC2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */; }; @@ -2861,6 +2863,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 */; }; + BB9BDD492D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */; }; + BB9BDD4A2D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */; }; BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; BBB881892C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; BBBB65402C77BB9400E69AC6 /* BookmarkSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */; }; @@ -4820,12 +4824,14 @@ B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = ""; }; B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = ""; }; BB0346F42CEB80B400D23E05 /* DownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTests.swift; sourceTree = ""; }; + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageView.swift; sourceTree = ""; }; BB4339DA2C7F9606005D7ED7 /* PinnedTabsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTabsTests.swift; sourceTree = ""; }; BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewModel.swift; sourceTree = ""; }; BB5789712B2CA70F0009DFE2 /* DataBrokerProtectionSubscriptionEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataBrokerProtectionSubscriptionEventHandler.swift; sourceTree = ""; }; 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 = ""; }; + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModel.swift; sourceTree = ""; }; BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerSearchDataSource.swift; sourceTree = ""; }; BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchTests.swift; sourceTree = ""; }; BBBEE1BE2C4FF63600035ABA /* SortBookmarksViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortBookmarksViewModelTests.swift; sourceTree = ""; }; @@ -8270,6 +8276,7 @@ AA86491124D8318F001BABEE /* TabBar */ = { isa = PBXGroup; children = ( + BB68B6542D0B1E9200CEC812 /* TabBarRemoteMessaging */, AA86491224D831A1001BABEE /* View */, AA8EDF1F2491FCC10071C2E8 /* ViewModel */, AA9FF95724A1ECE20039E328 /* Model */, @@ -9470,6 +9477,15 @@ path = View; sourceTree = ""; }; + BB68B6542D0B1E9200CEC812 /* TabBarRemoteMessaging */ = { + isa = PBXGroup; + children = ( + BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */, + BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */, + ); + path = TabBarRemoteMessaging; + sourceTree = ""; + }; BD7090D42C540C0D009EED82 /* MetadataCollectors */ = { isa = PBXGroup; children = ( @@ -11637,6 +11653,7 @@ C1C405882C7F80E50089DE8A /* PromotionView+FreemiumDBP.swift in Sources */, B6E1491029A5C30500AAFBE8 /* ContentBlockingTabExtension.swift in Sources */, 3706FB82293F65D500E42796 /* PasswordManagementNoteItemView.swift in Sources */, + BB9BDD492D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */, 4BF97AD82B43C5B300EB4240 /* NetworkProtectionAppEvents.swift in Sources */, 3706FEC5293F6F0600E42796 /* BWInstallationService.swift in Sources */, BDBA85972C5D256C00BC54F5 /* VPNFeedbackFormView.swift in Sources */, @@ -11925,6 +11942,7 @@ F1B33DF32BAD929D001128B3 /* SubscriptionAppStoreRestorer.swift in Sources */, 3706FC1F293F65D500E42796 /* BookmarksBarViewController.swift in Sources */, 1DDC85042B83903E00670238 /* PreferencesWebTrackingProtectionView.swift in Sources */, + BB3229062D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */, 3706FC20293F65D500E42796 /* PreferencesAutofillView.swift in Sources */, CD2AB5C22C8222F50019EB49 /* MaliciousSiteProtectionPreferences.swift in Sources */, 3706FC21293F65D500E42796 /* UserText+PasswordManager.swift in Sources */, @@ -13104,6 +13122,7 @@ 37D0469F2C7D0EDD00AEAA50 /* CustomBackground.swift in Sources */, B684592225C93BE000DC17B6 /* Publisher.asVoid.swift in Sources */, 4B9DB01D2A983B24000927DB /* Waitlist.swift in Sources */, + BB9BDD4A2D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */, AAA0CC33252F181A0079BC96 /* NavigationButtonMenuDelegate.swift in Sources */, 1DA84D2F2C11989D0011C80F /* Update.swift in Sources */, AAC30A2A268E239100D2D9CD /* CrashReport.swift in Sources */, @@ -13714,6 +13733,7 @@ 4B59024826B3673600489384 /* ThirdPartyBrowser.swift in Sources */, B60C6F7729B0E286007BFAA8 /* SearchNonexistentDomainNavigationResponder.swift in Sources */, B65E6B9E26D9EC0800095F96 /* CircularProgressView.swift in Sources */, + BB3229052D08644400DA92E9 /* TabBarRemoteMessageView.swift in Sources */, 56A053FC2C19E8F7007D8FAB /* OnboardingActionsManager.swift in Sources */, EEE50C292C38249C003DD7FF /* OptionalExtension.swift in Sources */, AABEE69C24A902BB0043105B /* SuggestionContainer.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json new file mode 100644 index 0000000000..50d23b7933 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Response-DDG-Question-96x96.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg new file mode 100644 index 0000000000..e3d009683d --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/dax-response.imageset/Response-DDG-Question-96x96.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Extensions/URLExtension.swift b/DuckDuckGo/Common/Extensions/URLExtension.swift index a49d39f36d..d6f8cf11d6 100644 --- a/DuckDuckGo/Common/Extensions/URLExtension.swift +++ b/DuckDuckGo/Common/Extensions/URLExtension.swift @@ -371,6 +371,10 @@ extension URL { return URL(string: "https://duckduckgo.com/updates")! } + static var survey: URL { + return URL(string: "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2")! + } + static var webTrackingProtection: URL { return URL(string: "https://help.duckduckgo.com/duckduckgo-help-pages/privacy/web-tracking-protections/")! } diff --git a/DuckDuckGo/HomePage/View/HomePageView.swift b/DuckDuckGo/HomePage/View/HomePageView.swift index 44c71a6288..8fe1f5ff25 100644 --- a/DuckDuckGo/HomePage/View/HomePageView.swift +++ b/DuckDuckGo/HomePage/View/HomePageView.swift @@ -183,7 +183,10 @@ extension HomePage.Views { @ViewBuilder func remoteMessage() -> some View { - if let remoteMessage = activeRemoteMessageModel.remoteMessage, let modelType = remoteMessage.content, modelType.isSupported { + if let remoteMessage = activeRemoteMessageModel.remoteMessage, + !remoteMessage.isForTabBar, + let modelType = remoteMessage.content, + modelType.isSupported { ZStack { RemoteMessageView(viewModel: .init( messageId: remoteMessage.id, diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index f90ccc2115..38bbffa1d4 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -64606,4 +64606,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift index d0674088c2..ffc06c00aa 100644 --- a/DuckDuckGo/MainWindow/MainViewController.swift +++ b/DuckDuckGo/MainWindow/MainViewController.swift @@ -75,7 +75,7 @@ final class MainViewController: NSViewController { self.isBurner = tabCollectionViewModel.isBurner self.featureFlagger = featureFlagger - tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel) + tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, activeRemoteMessageModel: NSApp.delegateTyped.activeRemoteMessageModel) bookmarksBarVisibilityManager = BookmarksBarVisibilityManager(selectedTabPublisher: tabCollectionViewModel.$selectedTabViewModel.eraseToAnyPublisher()) let networkProtectionPopoverManager: NetPPopoverManager = { diff --git a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift index 26b617934d..c6d7c7b8fb 100644 --- a/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift +++ b/DuckDuckGo/NewTabPage/ActiveRemoteMessageModel+NewTabPage.swift @@ -22,7 +22,10 @@ import RemoteMessaging extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding { var remoteMessagePublisher: AnyPublisher { - $remoteMessage.dropFirst().eraseToAnyPublisher() + $remoteMessage + .dropFirst() + .filter { $0?.isForTabBar == true } + .eraseToAnyPublisher() } func isMessageSupported(_ message: RemoteMessageModel) -> Bool { diff --git a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift index 0af0f67f97..bf933c1774 100644 --- a/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift +++ b/DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift @@ -185,3 +185,10 @@ extension RemoteMessageModelType { } } } + +extension RemoteMessageModel { + + var isForTabBar: Bool { + return id == TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId + } +} 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/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift new file mode 100644 index 0000000000..3f7d240f57 --- /dev/null +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -0,0 +1,163 @@ +// +// TabBarRemoteMessageView.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 + +struct TabBarRemoteMessageView: View { + @State private var presentPopup: Bool = false + + let model: TabBarRemoteMessage + let onClose: () -> Void + let onTap: (URL) -> Void + let onHover: () -> Void + + var body: some View { + HStack { + Button(model.buttonTitle) { + onTap(model.surveyURL) + } + .buttonStyle(DefaultActionButtonStyle( + enabled: true, + onClose: { onClose() }, + onHoverStart: { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + presentPopup = true + + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + presentPopup = false + } + } + + onHover() + }, + onHoverEnd: { presentPopup = false }) + ) + .frame(width: 147, height: 24) + .popover(isPresented: $presentPopup, arrowEdge: .bottom) { + HStack(alignment: .center) { + Image(.daxResponse) + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + .padding(.leading, 12) + + VStack(alignment: .leading) { + Text(model.popupTitle) + .font(.body) + .fontWeight(.bold) + .frame(alignment: .leading) + .padding(.bottom, 12) + + Text(model.popupSubtitle) + .font(.body) + .fontWeight(.medium) + .frame(alignment: .leading) + + } + .frame(maxWidth: 360, minHeight: 90) + .padding(.trailing, 24) + .padding(.leading, 4) + } + } + } + } +} + +private struct DefaultActionButtonStyle: ButtonStyle { + + public let enabled: Bool + public let onClose: () -> Void + public let onHoverStart: () -> Void + public let onHoverEnd: () -> Void + + public init( + enabled: Bool, + onClose: @escaping () -> Void, + onHoverStart: @escaping () -> Void = {}, + onHoverEnd: @escaping () -> Void = {} + ) { + self.enabled = enabled + self.onClose = onClose + self.onHoverStart = onHoverStart + self.onHoverEnd = onHoverEnd + } + + public func makeBody(configuration: Self.Configuration) -> some View { + ButtonContent( + configuration: configuration, + enabled: enabled, + onClose: onClose, + onHoverStart: onHoverStart, + onHoverEnd: onHoverEnd + ) + } + + struct ButtonContent: View { + let configuration: Configuration + let enabled: Bool + let onClose: () -> Void + let onHoverStart: () -> Void + let onHoverEnd: () -> Void + + @State private var isHovered: Bool = false + + var body: some View { + let enabledBackgroundColor = configuration.isPressed + ? Color("PrimaryButtonPressed") + : (isHovered + ? Color("PrimaryButtonHover") + : Color("PrimaryButtonRest")) + + let disabledBackgroundColor = Color.gray.opacity(0.1) + let enabledLabelColor = configuration.isPressed ? Color.white.opacity(0.8) : Color.white + let disabledLabelColor = Color.primary.opacity(0.3) + + HStack(spacing: 5) { + configuration.label + .font(.system(size: 13)) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + + Button(action: { + onClose() + }) { + Image(.close) + .resizable() + .frame(width: 12, height: 12) + .foregroundColor(enabled ? Color.white : Color.primary.opacity(0.3)) + } + .buttonStyle(PlainButtonStyle()) // Avoids additional styling + } + .frame(minWidth: 44) + .padding(.top, 2.5) + .padding(.bottom, 3) + .padding(.horizontal, 7.5) + .background(enabled ? enabledBackgroundColor : disabledBackgroundColor) + .foregroundColor(enabled ? enabledLabelColor : disabledLabelColor) + .cornerRadius(5) + .onHover { hovering in + isHovered = hovering + if hovering { + onHoverStart() + } else { + onHoverEnd() + } + } + } + } +} diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift new file mode 100644 index 0000000000..9547215b1c --- /dev/null +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift @@ -0,0 +1,98 @@ +// +// TabBarRemoteMessageViewModel.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 Combine +import RemoteMessaging + +struct TabBarRemoteMessage { + static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar" + + let buttonTitle: String + let popupTitle: String + let popupSubtitle: String + let surveyURL: URL +} + +final class TabBarRemoteMessageViewModel: ObservableObject { + + private let activeRemoteMessageModel: ActiveRemoteMessageModel + private var cancellable: AnyCancellable? + + @Published var remoteMessage: TabBarRemoteMessage? + + init(activeRemoteMessageModel: ActiveRemoteMessageModel) { + self.activeRemoteMessageModel = activeRemoteMessageModel + + cancellable = activeRemoteMessageModel.$remoteMessage + .sink(receiveValue: { model in + guard let model = model else { + self.remoteMessage = nil + return + } + + if model.shouldShowTabBarRemoteMessage, let tabBarRemoteMessage = model.mapToTabBarRemoteMessage() { + self.remoteMessage = tabBarRemoteMessage + } + }) + } + + func onDismiss() { + Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .close) } + } + + /// When the user hovers the Tab Bar Remote Message and we show the popup, there is where when we mark + /// that the user really saw the message. + func onUserHovered() { + Task { await activeRemoteMessageModel.markRemoteMessageAsShown() } + } + + func onOpenSurvey() { + Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction) } + } +} + +private extension RemoteMessageModel { + + var shouldShowTabBarRemoteMessage: Bool { + guard let modelType = content else { return false } + + return modelType.isSupported && isForTabBar + } + + func mapToTabBarRemoteMessage() -> TabBarRemoteMessage? { + guard let modelType = content else { return nil } + + switch modelType { + case .bigSingleAction(let titleText, + let descriptionText, + _, + let primaryActionText, + let primaryAction): + + if case .survey(let value) = primaryAction, let surveyURL = URL(string: value) { + return .init(buttonTitle: titleText, + popupTitle: primaryActionText, + popupSubtitle: descriptionText, + surveyURL: surveyURL) + } else { + return nil + } + default: return nil + } + } +} diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index f9038f0b94..d10e67df2d 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -23,6 +23,7 @@ import Lottie import SwiftUI import WebKit import os.log +import RemoteMessaging final class TabBarViewController: NSViewController { @@ -70,6 +71,9 @@ final class TabBarViewController: NSViewController { private let pinnedTabsViewModel: PinnedTabsViewModel? private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? + private let tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel + private let feedbackPopoverViewController: PopoverMessageViewController + private var feedbackBarButtonHostingController: NSHostingController? private var selectionIndexCancellable: AnyCancellable? private var mouseDownCancellable: AnyCancellable? @@ -86,9 +90,9 @@ final class TabBarViewController: NSViewController { } } - static func create(tabCollectionViewModel: TabCollectionViewModel) -> TabBarViewController { + static func create(tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) -> TabBarViewController { NSStoryboard(name: "TabBar", bundle: nil).instantiateInitialController { coder in - self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel) + self.init(coder: coder, tabCollectionViewModel: tabCollectionViewModel, activeRemoteMessageModel: activeRemoteMessageModel) }! } @@ -96,8 +100,9 @@ final class TabBarViewController: NSViewController { fatalError("TabBarViewController: Bad initializer") } - init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel) { + init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) { self.tabCollectionViewModel = tabCollectionViewModel + self.tabBarRemoteMessageViewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: activeRemoteMessageModel) if !tabCollectionViewModel.isBurner, let pinnedTabCollection = tabCollectionViewModel.pinnedTabsManager?.tabCollection { let pinnedTabsViewModel = PinnedTabsViewModel(collection: pinnedTabCollection) let pinnedTabsView = PinnedTabsView(model: pinnedTabsViewModel) @@ -110,6 +115,15 @@ final class TabBarViewController: NSViewController { self.pinnedTabsHostingView = nil } + feedbackPopoverViewController = PopoverMessageViewController( + title: "Tell Us What You Think", + message: "Take our short survey and help us build the best browser.", + image: .daxResponse, + shouldShowCloseButton: false, + presentMultiline: true, + autoDismissDuration: nil + ) + super.init(coder: coder) } @@ -124,6 +138,7 @@ final class TabBarViewController: NSViewController { subscribeToTabModeChanges() setupAddTabButton() setupAsBurnerWindowIfNeeded() + addTabBarRemoteMessageListener() } override func viewWillAppear() { @@ -200,6 +215,61 @@ final class TabBarViewController: NSViewController { } } + private func addTabBarRemoteMessageListener() { + tabBarRemoteMessageViewModel.$remoteMessage.sink(receiveValue: { tabBarRemoteMessage in + if let tabBarRemoteMessage = tabBarRemoteMessage { + if self.feedbackBarButtonHostingController == nil { + self.showTabBarRemoteMessage(tabBarRemoteMessage) + } + } else { + if self.feedbackBarButtonHostingController != nil { + self.removeFeedbackButton() + } + } + }) + .store(in: &cancellables) + } + + private func showTabBarRemoteMessage(_ tabBarRemotMessage: TabBarRemoteMessage) { + let feedbackButtonView = TabBarRemoteMessageView( + model: tabBarRemotMessage, + onClose: { + self.tabBarRemoteMessageViewModel.onDismiss() + self.removeFeedbackButton() + }, + onTap: { surveyURL in + WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) + self.tabBarRemoteMessageViewModel.onOpenSurvey() + self.removeFeedbackButton() + }, + onHover: { + self.tabBarRemoteMessageViewModel.onUserHovered() + } + ) + 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.heightAnchor.constraint(equalToConstant: 24), + feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) + ]) + } + + 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() From e01f0ab90ca4d0a5cb51eece4e9aaf88f5c24d86 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 18:05:38 -0300 Subject: [PATCH 2/9] Change production URL --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 265658a8a0..142a1238cb 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -49,7 +49,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { #if DEBUG URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else - URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #endif }() } From b5cd437bd1bfbfa014deca7a070dc376f22d3024 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 18:35:25 -0300 Subject: [PATCH 3/9] Add JSON to be used as debug --- tab-bar-remote-json-endpoint.json | 231 ++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 tab-bar-remote-json-endpoint.json diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json new file mode 100644 index 0000000000..2493aaf771 --- /dev/null +++ b/tab-bar-remote-json-endpoint.json @@ -0,0 +1,231 @@ +{ + "version": 9, + "messages": [ + { + "id": "macos_permanent_survey_tab_bar", + "content": { + "messageType": "big_single_action", + "titleText": "Help Us Improve", + "descriptionText": "Take our short survey and help us build the best browser.", + "placeholder": "Announce", + "primaryActionText": "Share Your Thoughts", + "primaryAction": { + "type": "survey", + "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", + "additionalParameters": { + "queryParams": "delta;var;osv;ddgv" + } + } + }, + "matchingRules": [] + } + ], + "rules": [ + { + "id": 1, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproPurchasePlatform": { + "value": [ + "apple" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "expiring" + ] + }, + "pproDaysUntilExpiryOrRenewal": { + "max": 10 + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": true + } + } + }, + { + "id": 2, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproPurchasePlatform": { + "value": [ + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "expiring" + ] + }, + "pproDaysUntilExpiryOrRenewal": { + "max": 10 + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 3, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproDaysSinceSubscribed": { + "min": 14 + }, + "pproPurchasePlatform": { + "value": [ + "apple", + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": true + } + } + }, + { + "id": 4, + "attributes": { + "pproSubscriber": { + "value": true + }, + "pproDaysSinceSubscribed": { + "min": 14 + }, + "pproPurchasePlatform": { + "value": [ + "apple", + "stripe" + ] + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + }, + "appVersion": { + "min": "1.101.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 5, + "attributes": { + "interactedWithMessage": { + "value": [ + "macos_privacy_pro_exit_survey_1", + "macos_privacy_pro_sparkle_exit_survey_1", + "macos_privacy_pro_app_store_exit_survey_1" + ] + } + } + }, + { + "id": 6, + "attributes": { + "interactedWithMessage": { + "value": [ + "macos_privacy_pro_subscriber_survey_1" + ] + } + } + }, + { + "id": 7, + "attributes": { + "interactedWithDeprecatedMacRemoteMessage": { + "value": [ + "privacy_pro_exit_survey_1" + ] + } + } + }, + { + "id": 8, + "attributes": { + "interactedWithDeprecatedMacRemoteMessage": { + "value": [ + "privacy_pro_survey_1" + ] + } + } + }, + { + "id": 9, + "attributes": { + "appVersion": { + "min": "1.106.0" + }, + "daysSinceInstalled": { + "min": 14, + "max": 21 + }, + "locale": { + "value": [ + "en-US", + "en-CA", + "en-GB", + "en-AU" + ] + } + } + }, + { + "id": 10, + "attributes": { + "appVersion": { + "min": "1.99.0", + "max": "1.102.0" + }, + "installedMacAppStore": { + "value": false + } + } + }, + { + "id": 11, + "attributes": { + "appVersion": { + "min": "1.116.0.0", + "max": "1.116.0.999999" + }, + "locale": { + "value": [ + "en-US" + ] + }, + "pproSubscriber": { + "value": true + }, + "pproSubscriptionStatus": { + "value": [ + "active" + ] + } + } + } + ] +} From 8fa6fae4e57be03655ce335abd2e66deab694640 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 18:39:07 -0300 Subject: [PATCH 4/9] Use raw JSON --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 142a1238cb..91ea2b7288 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -47,9 +47,9 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30 static let endpoint: URL = { #if DEBUG - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! #else - URL(string: "https://www.jsonblob.com/api/1316017217598578688")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! #endif }() } From e4ce594cbf94248886deefdbe3aa65240f0df359 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 12 Dec 2024 19:31:37 -0300 Subject: [PATCH 5/9] Make button as tall as the tab bar --- .../TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index 3f7d240f57..27341fba65 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -47,7 +47,7 @@ struct TabBarRemoteMessageView: View { }, onHoverEnd: { presentPopup = false }) ) - .frame(width: 147, height: 24) + .frame(width: 147) .popover(isPresented: $presentPopup, arrowEdge: .bottom) { HStack(alignment: .center) { Image(.daxResponse) From 9406d58256dd3e13b99f3eeb8c9d16faf1e71489 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Fri, 13 Dec 2024 09:47:27 -0300 Subject: [PATCH 6/9] Minor feedback --- .../RemoteMessagingClient.swift | 2 +- .../TabBarRemoteMessageView.swift | 87 ++++++++++--------- .../TabBar/View/TabBarViewController.swift | 1 - tab-bar-remote-json-endpoint.json | 6 +- 4 files changed, 51 insertions(+), 45 deletions(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 91ea2b7288..06d5b1ac05 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/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! + URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! #endif diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index 27341fba65..d6495c8bc0 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -20,6 +20,7 @@ import SwiftUI struct TabBarRemoteMessageView: View { @State private var presentPopup: Bool = false + @State private var hoverTimer: Timer? let model: TabBarRemoteMessage let onClose: () -> Void @@ -35,45 +36,55 @@ struct TabBarRemoteMessageView: View { enabled: true, onClose: { onClose() }, onHoverStart: { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - presentPopup = true - - DispatchQueue.main.asyncAfter(deadline: .now() + 3) { - presentPopup = false - } - } - + startHoverTimer() onHover() }, - onHoverEnd: { presentPopup = false }) + onHoverEnd: { + cancelHoverTimer() + }) ) .frame(width: 147) .popover(isPresented: $presentPopup, arrowEdge: .bottom) { - HStack(alignment: .center) { - Image(.daxResponse) - .resizable() - .scaledToFit() - .frame(width: 72, height: 72) - .padding(.leading, 12) - - VStack(alignment: .leading) { - Text(model.popupTitle) - .font(.body) - .fontWeight(.bold) - .frame(alignment: .leading) - .padding(.bottom, 12) - - Text(model.popupSubtitle) - .font(.body) - .fontWeight(.medium) - .frame(alignment: .leading) - - } - .frame(maxWidth: 360, minHeight: 90) - .padding(.trailing, 24) - .padding(.leading, 4) - } + PopoverContent(model: model) + } + } + } + + private func startHoverTimer() { + hoverTimer?.invalidate() + hoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + presentPopup = true + } + } + + private func cancelHoverTimer() { + hoverTimer?.invalidate() + presentPopup = false + } +} + +struct PopoverContent: View { + let model: TabBarRemoteMessage + + var body: some View { + HStack(alignment: .center) { + Image(.daxResponse) + .resizable() + .scaledToFit() + .frame(width: 72, height: 72) + .padding(.leading, 12) + + VStack(alignment: .leading) { + Text(model.popupTitle) + .font(.system(size: 13, weight: .bold)) + .padding(.bottom, 8) + + Text(model.popupSubtitle) + .font(.system(size: 13, weight: .regular)) } + .frame(width: 360, height: 92) + .padding(.trailing, 24) + .padding(.leading, 4) } } } @@ -133,15 +144,11 @@ private struct DefaultActionButtonStyle: ButtonStyle { .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) - Button(action: { - onClose() - }) { + Button(action: { onClose() }) { Image(.close) - .resizable() - .frame(width: 12, height: 12) - .foregroundColor(enabled ? Color.white : Color.primary.opacity(0.3)) } - .buttonStyle(PlainButtonStyle()) // Avoids additional styling + .frame(width: 16, height: 16) + .buttonStyle(PlainButtonStyle()) } .frame(minWidth: 44) .padding(.top, 2.5) diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index d10e67df2d..1beb645786 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -256,7 +256,6 @@ final class TabBarViewController: NSViewController { rightSideStackView.insertArrangedSubview(feedbackBarButtonHostingController.view, at: index) NSLayoutConstraint.activate([ - feedbackBarButtonHostingController.view.heightAnchor.constraint(equalToConstant: 24), feedbackBarButtonHostingController.view.centerYAnchor.constraint(equalTo: rightSideStackView.centerYAnchor) ]) } diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json index 2493aaf771..c9ab320017 100644 --- a/tab-bar-remote-json-endpoint.json +++ b/tab-bar-remote-json-endpoint.json @@ -1,4 +1,4 @@ -{ +i{ "version": 9, "messages": [ { @@ -6,9 +6,9 @@ "content": { "messageType": "big_single_action", "titleText": "Help Us Improve", - "descriptionText": "Take our short survey and help us build the best browser.", + "descriptionText": "We really want to know which features would make our browser better.", "placeholder": "Announce", - "primaryActionText": "Share Your Thoughts", + "primaryActionText": "Tell Us What You Think", "primaryAction": { "type": "survey", "value": "https://selfserve.decipherinc.com/survey/selfserve/32ab/241004?list=2", From 371be17dd853297b7296aa07c2ac31baa861f56c Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 15 Dec 2024 19:52:57 -0300 Subject: [PATCH 7/9] More improvements --- .../TabBarRemoteMessageView.swift | 40 ++++++-------- .../TabBar/View/TabBarViewController.swift | 54 +++++++++++++++---- 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index d6495c8bc0..3bf4c5b6b4 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -20,12 +20,12 @@ import SwiftUI struct TabBarRemoteMessageView: View { @State private var presentPopup: Bool = false - @State private var hoverTimer: Timer? let model: TabBarRemoteMessage let onClose: () -> Void let onTap: (URL) -> Void let onHover: () -> Void + let onHoverEnd: () -> Void var body: some View { HStack { @@ -36,56 +36,46 @@ struct TabBarRemoteMessageView: View { enabled: true, onClose: { onClose() }, onHoverStart: { - startHoverTimer() onHover() }, onHoverEnd: { - cancelHoverTimer() + onHoverEnd() }) ) .frame(width: 147) - .popover(isPresented: $presentPopup, arrowEdge: .bottom) { - PopoverContent(model: model) - } - } - } - - private func startHoverTimer() { - hoverTimer?.invalidate() - hoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in - presentPopup = true } } +} - private func cancelHoverTimer() { - hoverTimer?.invalidate() - presentPopup = false +struct TabBarRemoteMessagePopoverContent: View { + enum Constants { + static let height: CGFloat = 92 + static let width: CGFloat = 360 } -} -struct PopoverContent: View { let model: TabBarRemoteMessage var body: some View { - HStack(alignment: .center) { + HStack(alignment: .center, spacing: 0) { Image(.daxResponse) .resizable() .scaledToFit() .frame(width: 72, height: 72) - .padding(.leading, 12) + .padding(.leading, 8) + .padding(.trailing, 16) - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { Text(model.popupTitle) .font(.system(size: 13, weight: .bold)) .padding(.bottom, 8) Text(model.popupSubtitle) - .font(.system(size: 13, weight: .regular)) + .font(.system(size: 13, weight: .medium)) } - .frame(width: 360, height: 92) - .padding(.trailing, 24) - .padding(.leading, 4) + .padding(.trailing, 12) + .padding([.bottom, .top], 10) } + .frame(width: Constants.width, height: Constants.height) } } diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 1beb645786..0bdd871fb7 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -72,7 +72,8 @@ final class TabBarViewController: NSViewController { private let pinnedTabsView: PinnedTabsView? private let pinnedTabsHostingView: PinnedTabsHostingView? private let tabBarRemoteMessageViewModel: TabBarRemoteMessageViewModel - private let feedbackPopoverViewController: PopoverMessageViewController + private var tabBarRemoteMessagePopover: NSPopover? + private var tabBarRemoteMessagePopoverHoverTimer: Timer? private var feedbackBarButtonHostingController: NSHostingController? private var selectionIndexCancellable: AnyCancellable? @@ -115,15 +116,6 @@ final class TabBarViewController: NSViewController { self.pinnedTabsHostingView = nil } - feedbackPopoverViewController = PopoverMessageViewController( - title: "Tell Us What You Think", - message: "Take our short survey and help us build the best browser.", - image: .daxResponse, - shouldShowCloseButton: false, - presentMultiline: true, - autoDismissDuration: nil - ) - super.init(coder: coder) } @@ -243,7 +235,11 @@ final class TabBarViewController: NSViewController { self.removeFeedbackButton() }, onHover: { + self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) self.tabBarRemoteMessageViewModel.onUserHovered() + }, + onHoverEnd: { + self.dismissTabBarRemoteMessagePopover() } ) feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) @@ -260,6 +256,44 @@ final class TabBarViewController: NSViewController { ]) } + private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + self.showTabBarRemotePopup(message) + } + } + + private func dismissTabBarRemoteMessagePopover() { + tabBarRemoteMessagePopoverHoverTimer?.invalidate() + tabBarRemoteMessagePopover?.close() + } + + private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { + if let popover = tabBarRemoteMessagePopover { + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + + popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } else { + tabBarRemoteMessagePopover = NSPopover() + tabBarRemoteMessagePopover?.animates = true + tabBarRemoteMessagePopover?.behavior = .semitransient + tabBarRemoteMessagePopover?.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, + height: TabBarRemoteMessagePopoverContent.Constants.height) + + let controller = NSViewController() + controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + tabBarRemoteMessagePopover?.contentViewController = controller + + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + + tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) + } + } + private func removeFeedbackButton() { guard let hostingController = feedbackBarButtonHostingController else { return } From a2ec2abcd9191af9e8ea562ba8682f4ea4564195 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 15 Dec 2024 20:11:21 -0300 Subject: [PATCH 8/9] Update JSON link --- DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift index 06d5b1ac05..e7b3a6e619 100644 --- a/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift +++ b/DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift @@ -49,7 +49,7 @@ final class RemoteMessagingClient: RemoteMessagingProcessing { #if DEBUG URL(string: "https://www.jsonblob.com/api/1316017217598578688")! #else - URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/b5cd437bd1bfbfa014deca7a070dc376f22d3024/tab-bar-remote-json-endpoint.json")! + URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/refs/heads/juan/poc-new-user-feedfack-point-of-action/tab-bar-remote-json-endpoint.json")! #endif }() } From bbf78bd9fc1330c065ac553b7bdc2f644fc0e572 Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Sun, 15 Dec 2024 20:32:26 -0300 Subject: [PATCH 9/9] Fix JSON --- tab-bar-remote-json-endpoint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tab-bar-remote-json-endpoint.json b/tab-bar-remote-json-endpoint.json index c9ab320017..4ab0822362 100644 --- a/tab-bar-remote-json-endpoint.json +++ b/tab-bar-remote-json-endpoint.json @@ -1,4 +1,4 @@ -i{ +{ "version": 9, "messages": [ {