diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 314db00224..ac52fe1d26 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1083,6 +1083,8 @@ 373D9B4829EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */; }; 373D9B4929EEAC1B00381FDD /* SyncMetadataDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */; }; 373FB4B32B4D6C4B004C88D6 /* PreferencesViews in Frameworks */ = {isa = PBXBuildFile; productRef = 373FB4B22B4D6C4B004C88D6 /* PreferencesViews */; }; + 374286252CC5940100E66323 /* HomePageSettingsVisibilityModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374286242CC593F900E66323 /* HomePageSettingsVisibilityModelTests.swift */; }; + 374286262CC5940100E66323 /* HomePageSettingsVisibilityModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374286242CC593F900E66323 /* HomePageSettingsVisibilityModelTests.swift */; }; 37445F992A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; 37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F982A1566420029F789 /* SyncDataProviders.swift */; }; 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */; }; @@ -3415,6 +3417,7 @@ 373A26962964CF0B0043FC57 /* TestsTargetsBase.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TestsTargetsBase.xcconfig; sourceTree = ""; }; 373B2F802C384DEB0013A94B /* ActiveRemoteMessageModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveRemoteMessageModelTests.swift; sourceTree = ""; }; 373D9B4729EEAC1B00381FDD /* SyncMetadataDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMetadataDatabase.swift; sourceTree = ""; }; + 374286242CC593F900E66323 /* HomePageSettingsVisibilityModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomePageSettingsVisibilityModelTests.swift; sourceTree = ""; }; 37445F982A1566420029F789 /* SyncDataProviders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDataProviders.swift; sourceTree = ""; }; 37445F9B2A1569F00029F789 /* SyncBookmarksAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncBookmarksAdapter.swift; sourceTree = ""; }; 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TabCollectionViewModelTests+WithoutPinnedTabsManager.swift"; sourceTree = ""; }; @@ -6503,6 +6506,7 @@ 569277C329DEE09D00B633EF /* ContinueSetUpModelTests.swift */, 56D145ED29E6DAD900E3488A /* DataImportProviderTests.swift */, 370270BF2C78EB13002E44E4 /* HomePageSettingsModelTests.swift */, + 374286242CC593F900E66323 /* HomePageSettingsVisibilityModelTests.swift */, 37D046A02C7DA9A200AEAA50 /* UserBackgroundImagesManagerTests.swift */, 376731842C7EF97400EB097B /* ColorSchemeLosslessStringConvertibleExtensionTests.swift */, 376731962C7F36AA00EB097B /* UserBackgroundImageTests.swift */, @@ -11651,6 +11655,7 @@ 3706FE2A293F661700E42796 /* SafariVersionReaderTests.swift in Sources */, 3706FE2B293F661700E42796 /* AtbParserTests.swift in Sources */, 3706FE2C293F661700E42796 /* PermissionStoreMock.swift in Sources */, + 374286262CC5940100E66323 /* HomePageSettingsVisibilityModelTests.swift in Sources */, 3706FE2D293F661700E42796 /* ChromiumFaviconsReaderTests.swift in Sources */, 3706FE2E293F661700E42796 /* LocalBookmarkManagerTests.swift in Sources */, 9F180D132B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */, @@ -13298,6 +13303,7 @@ B6AE39F129373AF200C37AA4 /* EmptyAttributionRulesProver.swift in Sources */, 4BB99D1126FE1A84001E4761 /* SafariBookmarksReaderTests.swift in Sources */, BBC063E82C5A9E4B007BDC18 /* BookmarkManagementDetailViewModelTests.swift in Sources */, + 374286252CC5940100E66323 /* HomePageSettingsVisibilityModelTests.swift in Sources */, 1DA860722BE3AE950027B813 /* DockPositionProviderTests.swift in Sources */, 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/SettingsOnboardingPopoverImage.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/HomePage/SettingsOnboardingPopoverImage.imageset/Contents.json new file mode 100644 index 0000000000..a6215a8d35 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/HomePage/SettingsOnboardingPopoverImage.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "Image.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/HomePage/SettingsOnboardingPopoverImage.imageset/Image.svg b/DuckDuckGo/Assets.xcassets/Images/HomePage/SettingsOnboardingPopoverImage.imageset/Image.svg new file mode 100644 index 0000000000..8b47aacb62 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/HomePage/SettingsOnboardingPopoverImage.imageset/Image.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 9e1920d6fd..56d19c287f 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -1177,6 +1177,8 @@ struct UserText { static let bookmarksBarPromptAccept = NSLocalizedString("bookmarks.bar.prompt.accept", value: "Show", comment: "Accept button label on bookmarks bar prompt") // MARK: Home Page Settings + static let homePageSettingsOnboardingTitle = NSLocalizedString("home.page.settings.onboarding.title", value: "Add extra personality to your new tab page", comment: "Home Page Settings Onboarding message title") + static let homePageSettingsOnboardingMessage = NSLocalizedString("home.page.settings.onboarding.message", value: "Customize the background, theme, and even what content you see. Give it a try!", comment: "Home Page Settings Onboarding message") static let homePageSettingsTitle = NSLocalizedString("home.page.settings.header", value: "Customize", comment: "Home Page Settings title") static let goToSettings = NSLocalizedString("home.page.settings.go.to.settings", value: "Go to Settings", comment: "Settings button caption") static let background = NSLocalizedString("home.page.settings.background", value: "Background", comment: "Section title in Home Page Settings to customization Home Page background") diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift index 98b26b7c07..04c38f1941 100644 --- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift +++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift @@ -135,6 +135,7 @@ public struct UserDefaultsWrapper { case homePageIsRecentActivityVisible = "home.page.is.recent.activity.visible" case homePageIsSearchBarVisible = "home.page.is.search.bar.visible" case homePageIsFirstSession = "home.page.is.first.session" + case homePageDidShowSettingsOnboarding = "home.page.did.show.settings.onboarding" case homePageUserBackgroundImages = "home.page.user.background.images" case homePageCustomBackground = "home.page.custom.background" case homePageLastPickedCustomColor = "home.page.last.picked.custom.color" diff --git a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift index efecc32433..cdec35634c 100644 --- a/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift +++ b/DuckDuckGo/HomePage/Model/HomePageSettings/HomePageSettingsModel.swift @@ -23,12 +23,37 @@ import PixelKit import SwiftUI import SwiftUIExtensions +protocol SettingsVisibilityModelPersistor { + var didShowSettingsOnboarding: Bool { get set } +} + +final class UserDefaultsSettingsVisibilityModelPersistor: SettingsVisibilityModelPersistor { + @UserDefaultsWrapper(key: .homePageDidShowSettingsOnboarding, defaultValue: false) + var didShowSettingsOnboarding: Bool +} + extension HomePage.Models { /** - * This tiny model is used by HomePageViewController to expose a setting to control settings visibility + * This tiny model is used by HomePageViewController to expose a setting to control settings visibility, + * as well as to keep track of the settings onboarding popover. */ final class SettingsVisibilityModel: ObservableObject { @Published var isSettingsVisible: Bool = false + + var didShowSettingsOnboarding: Bool { + get { + persistor.didShowSettingsOnboarding + } + set { + persistor.didShowSettingsOnboarding = newValue + } + } + + init(persistor: SettingsVisibilityModelPersistor = UserDefaultsSettingsVisibilityModelPersistor()) { + self.persistor = persistor + } + + private var persistor: SettingsVisibilityModelPersistor } final class SettingsModel: ObservableObject { @@ -64,6 +89,7 @@ extension HomePage.Models { let showAddImageFailedAlert: () -> Void let navigator: HomePageSettingsModelNavigator + @Published var settingsButtonWidth: CGFloat = .infinity @Published private(set) var availableUserBackgroundImages: [UserBackgroundImage] = [] private var availableCustomImagesCancellable: AnyCancellable? diff --git a/DuckDuckGo/HomePage/View/HomePageView.swift b/DuckDuckGo/HomePage/View/HomePageView.swift index 8a53b894dc..fb68289c2b 100644 --- a/DuckDuckGo/HomePage/View/HomePageView.swift +++ b/DuckDuckGo/HomePage/View/HomePageView.swift @@ -27,6 +27,7 @@ extension HomePage.Views { static let targetWidth: CGFloat = 508 static let minWindowWidth: CGFloat = 660 static let settingsPanelWidth: CGFloat = 236 + static let customizeButtonPadding: CGFloat = 14 let isBurner: Bool @EnvironmentObject var model: AppearancePreferences @@ -114,7 +115,7 @@ extension HomePage.Views { Spacer() HStack { Spacer(minLength: Self.targetWidth + (geometry.size.width - Self.targetWidth)/2) - SettingsButtonView(isSettingsVisible: $settingsVisibilityModel.isSettingsVisible) + SettingsButtonView() .padding([.bottom, .trailing], 14) } } @@ -236,25 +237,28 @@ extension HomePage.Views { } struct SettingsButtonView: View { - let defaultColor: Color = .homeFavoritesBackground - let onHoverColor: Color = .buttonMouseOver - let onSelectedColor: Color = .buttonMouseDown - let iconSize = 16.02 - let targetSize = 28.0 - let buttonWidthWithoutTitle = 52.0 + static let defaultColor: Color = .homeFavoritesBackground + static let onHoverColor: Color = .buttonMouseOver + static let iconSize = 16.0 + static let height = 28.0 + static let buttonWidthWithoutTitle = 46.0 @State var isHovering: Bool = false - @Binding var isSettingsVisible: Bool - @State private var textWidth: CGFloat = .infinity + @State private var textWidth: CGFloat = .infinity { + didSet { + settingsModel.settingsButtonWidth = textWidth + Self.buttonWidthWithoutTitle + } + } @EnvironmentObject var settingsModel: HomePage.Models.SettingsModel + @EnvironmentObject var settingsVisibilityModel: HomePage.Models.SettingsVisibilityModel private var buttonBackgroundColor: Color { - isHovering ? onHoverColor : defaultColor + isHovering ? Self.onHoverColor : Self.defaultColor } private func isCompact(with geometry: GeometryProxy) -> Bool { - geometry.size.width < textWidth + buttonWidthWithoutTitle + geometry.size.width < settingsModel.settingsButtonWidth } var body: some View { @@ -276,7 +280,7 @@ extension HomePage.Views { HStack(spacing: 6) { Image(.optionsMainView) .resizable() - .frame(width: iconSize, height: iconSize) + .frame(width: Self.iconSize, height: Self.iconSize) .scaledToFit() if !isCompact(with: geometry) { Text(UserText.homePageSettingsTitle) @@ -284,13 +288,13 @@ extension HomePage.Views { .background(WidthGetter()) } } - .frame(height: targetSize) + .frame(height: Self.height) .padding(.horizontal, isCompact(with: geometry) ? 6 : 12) } .fixedSize() .link(onHoverChanged: nil) { withAnimation { - isSettingsVisible.toggle() + settingsVisibilityModel.isSettingsVisible.toggle() } } .onHover { isHovering in diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift index 11999f30a4..702c57b6f2 100644 --- a/DuckDuckGo/HomePage/View/HomePageViewController.swift +++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift @@ -127,12 +127,16 @@ final class HomePageViewController: NSViewController { super.viewDidAppear() refreshModels() addressBarModel.addressBarTextField?.makeMeFirstResponder() + + showSettingsOnboardingIfNeeded() } override func viewWillDisappear() { super.viewWillDisappear() historyCancellable = nil + + presentedViewControllers?.forEach { $0.dismiss() } } func refreshModelsOnAppBecomingActive() { @@ -271,6 +275,60 @@ final class HomePageViewController: NSViewController { .show(in: view.window) } + private func showSettingsOnboardingIfNeeded() { + if !settingsVisibilityModel.didShowSettingsOnboarding { + DispatchQueue.main.async { + guard let superview = self.view.superview else { + return + } + let bounds = self.view.bounds + let settingsButtonWidth = Application.appDelegate.homePageSettingsModel.settingsButtonWidth + + let rect = NSRect( + x: bounds.maxX - HomePage.Views.RootView.customizeButtonPadding - settingsButtonWidth, + y: bounds.maxY - HomePage.Views.RootView.customizeButtonPadding - HomePage.Views.RootView.SettingsButtonView.height, + width: settingsButtonWidth, + height: HomePage.Views.RootView.SettingsButtonView.height) + + // Create a helper view as anchor for the popover and align it with the 'Customize' button. + // This is to ensure that popover updates its position correctly as the window is resized. + let popoverAnchorView = NSView(frame: rect) + superview.addSubview(popoverAnchorView, positioned: .below, relativeTo: self.view) + popoverAnchorView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + popoverAnchorView.widthAnchor.constraint(equalToConstant: settingsButtonWidth), + popoverAnchorView.heightAnchor.constraint(equalToConstant: HomePage.Views.RootView.SettingsButtonView.height), + popoverAnchorView.trailingAnchor.constraint(equalTo: superview.trailingAnchor, constant: -HomePage.Views.RootView.customizeButtonPadding), + popoverAnchorView.bottomAnchor.constraint(equalTo: superview.bottomAnchor, constant: -HomePage.Views.RootView.customizeButtonPadding) + ]) + + let viewController = PopoverMessageViewController( + title: UserText.homePageSettingsOnboardingTitle, + message: UserText.homePageSettingsOnboardingMessage, + image: .settingsOnboardingPopover, + shouldShowCloseButton: true, + presentMultiline: true, + autoDismissDuration: nil, + onClick: { [weak self] in + self?.settingsVisibilityModel.isSettingsVisible = true + } + ) + viewController.show(onParent: self, relativeTo: popoverAnchorView, preferredEdge: .maxY) + self.settingsVisibilityModel.didShowSettingsOnboarding = true + + // Hide the popover as soon as settings is shown ('Customize' button is clicked). + self.settingsVisibilityModel.$isSettingsVisible + .filter { $0 } + .prefix(1) + .sink { [weak viewController] _ in + viewController?.dismiss() + popoverAnchorView.removeFromSuperview() + } + .store(in: &self.cancellables) + } + } + } + private var burningDataCancellable: AnyCancellable? private func subscribeToBurningData() { burningDataCancellable = fireViewModel.fire.$burningData diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index e9bd9d27ea..2acc2364e2 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -27292,6 +27292,30 @@ } } }, + "home.page.settings.onboarding.message" : { + "comment" : "Home Page Settings Onboarding message", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Customize the background, theme, and even what content you see. Give it a try!" + } + } + } + }, + "home.page.settings.onboarding.title" : { + "comment" : "Home Page Settings Onboarding message title", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Add extra personality to your new tab page" + } + } + } + }, "home.page.settings.sections" : { "comment" : "Section title in Home Page Settings to adjust Home Page sections visibility", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 6a5f42e49a..0469763c3b 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -627,6 +627,7 @@ final class MainMenu: NSMenu { NSMenuItem(title: "Reset CPM Experiment Cohort", action: #selector(AppDelegate.resetCpmCohort)) NSMenuItem(title: "Reset Duck Player Preferences", action: #selector(MainViewController.resetDuckPlayerPreferences)) NSMenuItem(title: "Reset Onboarding", action: #selector(MainViewController.resetOnboarding(_:))) + NSMenuItem(title: "Reset Home Page Settings Onboarding", action: #selector(MainViewController.resetHomePageSettingsOnboarding(_:))) NSMenuItem(title: "Reset Contextual Onboarding", action: #selector(MainViewController.resetContextualOnboarding(_:))) NSMenuItem(title: "Reset Sync Promo prompts", action: #selector(MainViewController.resetSyncPromoPrompts)) diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index d62b9f7502..ef742e932f 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -847,6 +847,10 @@ extension MainViewController { UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.onboardingFinished.rawValue) } + @objc func resetHomePageSettingsOnboarding(_ sender: Any?) { + UserDefaults.standard.set(false, forKey: UserDefaultsWrapper.Key.homePageDidShowSettingsOnboarding.rawValue) + } + @objc func resetContextualOnboarding(_ sender: Any?) { Application.appDelegate.onboardingStateMachine.state = .notStarted } diff --git a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift index 3e67e72352..3d63c1cddf 100644 --- a/DuckDuckGo/MessageViews/PopoverMessageViewController.swift +++ b/DuckDuckGo/MessageViews/PopoverMessageViewController.swift @@ -29,21 +29,23 @@ final class PopoverMessageViewController: NSHostingController Void)? - let autoDismissDuration: TimeInterval + let autoDismissDuration: TimeInterval? let onClick: (() -> Void)? private var timer: Timer? private var trackingArea: NSTrackingArea? - init(message: String, + init(title: String? = nil, + message: String, image: NSImage? = nil, buttonText: String? = nil, buttonAction: (() -> Void)? = nil, shouldShowCloseButton: Bool = false, presentMultiline: Bool = false, - autoDismissDuration: TimeInterval = Constants.autoDismissDuration, + autoDismissDuration: TimeInterval? = Constants.autoDismissDuration, onDismiss: (() -> Void)? = nil, onClick: (() -> Void)? = nil) { - self.viewModel = PopoverMessageViewModel(message: message, + self.viewModel = PopoverMessageViewModel(title: title, + message: message, image: image, buttonText: buttonText, buttonAction: buttonAction, @@ -76,18 +78,20 @@ final class PopoverMessageViewController: NSHostingController Void)? = nil, shouldShowCloseButton: Bool = false, shouldPresentMultiline: Bool = true) { + self.title = title self.message = message self.image = image self.buttonText = buttonText @@ -60,40 +63,91 @@ public struct PopoverMessageView: View { ZStack { ClickableViewRepresentable(onClick: onClick) .background(Color.clear) - HStack(alignment: .top) { - if let image = viewModel.image { - Image(nsImage: image) - .padding(.top, 3) - } + if let title = viewModel.title { + messageWithTitleBody(title) + } else { + messageBody + } + } + } + + @ViewBuilder + private var messageBody: some View { + HStack(alignment: .top) { + if let image = viewModel.image { + Image(nsImage: image) + .padding(.top, 3) + } - if viewModel.shouldPresentMultiline { - Text(viewModel.message) - .font(.body) - .fontWeight(.bold) - .padding(.leading, 2) - .frame(width: 150, alignment: .leading) - .frame(minHeight: 22) - .lineLimit(nil) - } else { - Text(viewModel.message) - .font(.body) - .fontWeight(.bold) - .padding(.leading, 2) - .frame(minHeight: 22) - .lineLimit(nil) + Text(viewModel.message) + .font(.body) + .fontWeight(.bold) + .padding(.leading, 2) + .frame(minHeight: 22) + .lineLimit(nil) + .if(viewModel.shouldPresentMultiline) { view in + view.frame(width: 150, alignment: .leading) } - if let text = viewModel.buttonText, - let action = viewModel.buttonAction { - Button(text, action: { - action() - onClose?() - }) - .padding(.top, 2) - .padding(.leading, 4) + if let text = viewModel.buttonText, + let action = viewModel.buttonAction { + Button(text, action: { + action() + onClose?() + }) + .padding(.top, 2) + .padding(.leading, 4) + } + + if viewModel.shouldShowCloseButton { + Button(action: { + onClose?() + }) { + Image(.updateNotificationClose) + .frame(width: 16, height: 16) } + .buttonStyle(PlainButtonStyle()) + .padding(.top, viewModel.buttonText != nil ? 4 : 0) + } + } + .padding() + } + + @ViewBuilder + private func messageWithTitleBody(_ title: String) -> some View { + HStack(spacing: 12) { + if let image = viewModel.image { + Image(nsImage: image) + } + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.body) + .fontWeight(.bold) + .frame(minHeight: 22) + .lineLimit(nil) + Text(viewModel.message) + .font(.body) + .frame(minHeight: 22) + .lineLimit(nil) + } + .padding(.leading, 8) + .if(viewModel.shouldPresentMultiline) { view in + view.frame(width: 300, alignment: .leading) + } - if viewModel.shouldShowCloseButton { + if let text = viewModel.buttonText, + let action = viewModel.buttonAction { + Button(text, action: { + action() + onClose?() + }) + .padding(.top, 2) + .padding(.leading, 4) + } + + if viewModel.shouldShowCloseButton { + VStack(spacing: 0) { Button(action: { onClose?() }) { @@ -101,11 +155,15 @@ public struct PopoverMessageView: View { .frame(width: 16, height: 16) } .buttonStyle(PlainButtonStyle()) - .padding(.top, viewModel.buttonText != nil ? 4 : 0) + .padding(.top, -4) + .padding(.trailing, -8) + + Spacer() } } - .padding() } + .padding(.horizontal) + .padding(.vertical, 12) } } diff --git a/UnitTests/HomePage/HomePageSettingsVisibilityModelTests.swift b/UnitTests/HomePage/HomePageSettingsVisibilityModelTests.swift new file mode 100644 index 0000000000..8cccccab48 --- /dev/null +++ b/UnitTests/HomePage/HomePageSettingsVisibilityModelTests.swift @@ -0,0 +1,42 @@ +// +// HomePageSettingsVisibilityModelTests.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 XCTest +@testable import DuckDuckGo_Privacy_Browser + +final class MockSettingsVisibilityModelPersistor: SettingsVisibilityModelPersistor { + var didShowSettingsOnboarding: Bool = false +} + +final class HomePageSettingsVisibilityModelTests: XCTestCase { + var persistor: MockSettingsVisibilityModelPersistor! + var model: HomePage.Models.SettingsVisibilityModel! + + override func setUp() { + super.setUp() + persistor = MockSettingsVisibilityModelPersistor() + model = .init(persistor: persistor) + } + + func testThatDidShowSettingsOnboardingIsPersisted() { + model.didShowSettingsOnboarding = true + XCTAssertTrue(persistor.didShowSettingsOnboarding) + model.didShowSettingsOnboarding = false + XCTAssertFalse(persistor.didShowSettingsOnboarding) + } +}