Skip to content

Commit

Permalink
Add support for background customization in HTML New Tab Page (#3711)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/72649045549333/1208246350498758/f

Description:
This change connects HomePageSettingsModel to HTML New Tab Page.
  • Loading branch information
ayoy authored Jan 10, 2025
1 parent da6c1e1 commit e961a41
Show file tree
Hide file tree
Showing 59 changed files with 2,412 additions and 685 deletions.
24 changes: 24 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "GradientTone02+01.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import Foundation

extension PromotionViewModel {
static func freemiumDBPPromotion(proceedAction: @escaping () -> Void,
static func freemiumDBPPromotion(proceedAction: @escaping () async -> Void,
closeAction: @escaping () -> Void) -> PromotionViewModel {

let title = UserText.homePagePromotionFreemiumDBPTitle
Expand All @@ -41,7 +41,7 @@ extension PromotionViewModel {

static func freemiumDBPPromotionScanEngagementResults(resultCount: Int,
brokerCount: Int,
proceedAction: @escaping () -> Void,
proceedAction: @escaping () async -> Void,
closeAction: @escaping () -> Void) -> PromotionViewModel {

var description = ""
Expand All @@ -65,7 +65,7 @@ extension PromotionViewModel {
closeAction: closeAction)
}

static func freemiumDBPPromotionScanEngagementNoResults(proceedAction: @escaping () -> Void,
static func freemiumDBPPromotionScanEngagementNoResults(proceedAction: @escaping () async -> Void,
closeAction: @escaping () -> Void) -> PromotionViewModel {

let description = UserText.homePagePromotionFreemiumDBPPostScanEngagementNoResultsDescription
Expand Down
1 change: 1 addition & 0 deletions DuckDuckGo/Freemium/DBP/FreemiumDBPPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Freemium

/// Conforming types provide functionality to show Freemium DBP
protocol FreemiumDBPPresenter {
@MainActor
func showFreemiumDBPAndSetActivated(windowControllerManager: WindowControllersManagerProtocol?)
}

Expand Down
30 changes: 22 additions & 8 deletions DuckDuckGo/Freemium/DBP/FreemiumDBPPromotionViewCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,14 @@ import Common

/// Default implementation of `FreemiumDBPPromotionViewCoordinating`, responsible for managing
/// the visibility of the promotion and responding to user interactions with the promotion view.
@MainActor
final class FreemiumDBPPromotionViewCoordinator: ObservableObject {

/// Published property that determines whether the promotion is visible on the home page.
@Published var isHomePagePromotionVisible: Bool = false

/// The view model representing the promotion, which updates based on the user's state. Returns `nil` if the feature is not enabled
var viewModel: PromotionViewModel? {
guard freemiumDBPFeature.isAvailable else { return nil }
return createViewModel()
}
@Published
private(set) var viewModel: PromotionViewModel?

/// Stores whether the user has dismissed the home page promotion.
private var didDismissHomePagePromotion: Bool {
Expand Down Expand Up @@ -89,14 +86,15 @@ final class FreemiumDBPPromotionViewCoordinator: ObservableObject {
setInitialPromotionVisibilityState()
subscribeToFeatureAvailabilityUpdates()
observeFreemiumDBPNotifications()
setUpViewModelRefreshing()
}
}

private extension FreemiumDBPPromotionViewCoordinator {

/// Action to be executed when the user proceeds with the promotion (e.g opens DBP)
var proceedAction: () -> Void {
{ [weak self] in
var proceedAction: () async -> Void {
{ @MainActor [weak self] in
guard let self else { return }

execute(resultsAction: {
Expand Down Expand Up @@ -130,6 +128,7 @@ private extension FreemiumDBPPromotionViewCoordinator {
}

/// Shows the Freemium DBP user interface via the presenter.
@MainActor
func showFreemiumDBP() {
freemiumDBPPresenter.showFreemiumDBPAndSetActivated(windowControllerManager: WindowControllersManager.shared)
}
Expand All @@ -148,7 +147,10 @@ private extension FreemiumDBPPromotionViewCoordinator {
/// Creates the view model for the promotion, updating based on the user's scan results.
///
/// - Returns: The `PromotionViewModel` that represents the current state of the promotion.
func createViewModel() -> PromotionViewModel {
func createViewModel() -> PromotionViewModel? {
guard freemiumDBPFeature.isAvailable, isHomePagePromotionVisible else {
return nil
}

if let results = freemiumDBPUserStateManager.firstScanResults {
if results.matchesCount > 0 {
Expand All @@ -172,12 +174,24 @@ private extension FreemiumDBPPromotionViewCoordinator {
}
}

/// This method defines the entry point to updating `viewModel` which is every change to `isHomePagePromotionVisible`.
func setUpViewModelRefreshing() {
$isHomePagePromotionVisible.dropFirst().asVoid()
.receive(on: DispatchQueue.main)
.sink { [weak self] in
self?.viewModel = self?.createViewModel()
}
.store(in: &cancellables)
}

/// Subscribes to feature availability updates from the `freemiumDBPFeature`'s availability publisher.
///
/// This method listens to the `isAvailablePublisher` of the `freemiumDBPFeature`, which publishes
/// changes to the feature's availability. It performs the following actions when an update is received:
func subscribeToFeatureAvailabilityUpdates() {
freemiumDBPFeature.isAvailablePublisher
.prepend(freemiumDBPFeature.isAvailable)
.removeDuplicates()
.receive(on: DispatchQueue.main)
.sink { [weak self] isAvailable in
guard let self else { return }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch

case gradient01
case gradient02
case gradient0201 = "gradient02.01"
case gradient03
case gradient04
case gradient05
Expand All @@ -46,6 +47,8 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch
Gradient01()
case .gradient02:
Gradient02()
case .gradient0201:
Gradient0201()
case .gradient03:
Gradient03()
case .gradient04:
Expand All @@ -68,6 +71,8 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch
Image(nsImage: .homePageBackgroundGradient01)
case .gradient02:
Image(nsImage: .homePageBackgroundGradient02)
case .gradient0201:
Image(nsImage: .homePageBackgroundGradient0201)
case .gradient03:
Image(nsImage: .homePageBackgroundGradient03)
case .gradient04:
Expand All @@ -83,7 +88,7 @@ enum GradientBackground: String, Equatable, Identifiable, CaseIterable, ColorSch

var colorScheme: ColorScheme {
switch self {
case .gradient01, .gradient02, .gradient03:
case .gradient01, .gradient02, .gradient0201, .gradient03:
.light
case .gradient04, .gradient05, .gradient06, .gradient07:
.dark
Expand Down Expand Up @@ -191,6 +196,35 @@ private struct Gradient02: View {
}
}

@available(macOS 12.0, *)
private struct Gradient0201: View {
var body: some View {
ZStack {
EllipticalGradient(
colors: [Color(red: 1, green: 0.8, blue: 0.2).opacity(0.8), .clear],
center: UnitPoint(x: 1.04, y: 1.08),
endRadiusFraction: 1
)

EllipticalGradient(
colors: [
Color(red: 1, green: 0.84, blue: 0.36).opacity(0.7),
Color(red: 1, green: 0.84, blue: 0.8).opacity(0.2)
],
center: UnitPoint(x: 0.56, y: 0.5),
endRadiusFraction: 1
)

EllipticalGradient(
colors: [Color(red: 0.95, green: 0.63, blue: 0.54).opacity(0.6), .clear],
center: UnitPoint(x: -0.26, y: 0.5),
endRadiusFraction: 1
)
}
.background(Color(red: 1, green: 0.87, blue: 0.48))
}
}

@available(macOS 12.0, *)
private struct Gradient03: View {
var body: some View {
Expand Down Expand Up @@ -320,6 +354,7 @@ private struct Gradient06: View {
.background(Color(red: 0.07, green: 0.01, blue: 0.21))
}
}

@available(macOS 12.0, *)
private struct Gradient07: View {
var body: some View {
Expand Down Expand Up @@ -358,6 +393,8 @@ private struct Gradient07: View {
.frame(width: 640, height: 400)
GradientBackground.gradient02.view
.frame(width: 640, height: 400)
GradientBackground.gradient0201.view
.frame(width: 640, height: 400)
GradientBackground.gradient03.view
.frame(width: 640, height: 400)
GradientBackground.gradient04.view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import Combine
import Foundation
import NewTabPage
import os.log
import PixelKit
import SwiftUI
Expand Down Expand Up @@ -59,7 +60,7 @@ extension HomePage.Models {
final class SettingsModel: ObservableObject {

enum Const {
static let maximumNumberOfUserImages = 4
static let maximumNumberOfUserImages = 8
static let defaultColorPickerColor = NSColor.white
}

Expand Down Expand Up @@ -88,6 +89,7 @@ extension HomePage.Models {
let userColorProvider: () -> UserColorProviding
let showAddImageFailedAlert: () -> Void
let navigator: HomePageSettingsModelNavigator
let customizerOpener = NewTabPageCustomizerOpener()

@Published var settingsButtonWidth: CGFloat = .infinity
@Published private(set) var availableUserBackgroundImages: [UserBackgroundImage] = []
Expand Down Expand Up @@ -244,8 +246,13 @@ extension HomePage.Models {
@Published var customBackground: CustomBackground? {
didSet {
appearancePreferences.homePageCustomBackground = customBackground
if case .userImage(let userBackgroundImage) = customBackground {
switch customBackground {
case .solidColor(let solidColorBackground) where solidColorBackground.predefinedColorName == nil:
lastPickedCustomColor = solidColorBackground.color
case .userImage(let userBackgroundImage):
customImagesManager?.updateSelectedTimestamp(for: userBackgroundImage)
default:
break
}
if let customBackground {
Logger.homePageSettings.debug("Home page background updated: \(customBackground), color scheme: \(customBackground.colorScheme)")
Expand Down Expand Up @@ -298,9 +305,6 @@ extension HomePage.Models {
provider.showColorPanel(with: lastPickedCustomColorHexValue.flatMap(NSColor.init(hex:)) ?? Const.defaultColorPickerColor)

userColorCancellable = provider.colorPublisher
.handleEvents(receiveOutput: { [weak self] color in
self?.lastPickedCustomColor = color
})
.map { CustomBackground.solidColor(.init(color: $0)) }
.assign(to: \.customBackground, onWeaklyHeld: self)
}
Expand Down
4 changes: 2 additions & 2 deletions DuckDuckGo/HomePage/Model/PromotionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ extension HomePage.Models {
let title: String?
let description: String
let proceedButtonText: String
let proceedAction: () -> Void
let proceedAction: () async -> Void
let closeAction: () -> Void

init(image: ImageResource, title: String? = nil, description: String, proceedButtonText: String, proceedAction: @escaping () -> Void, closeAction: @escaping () -> Void) {
init(image: ImageResource, title: String? = nil, description: String, proceedButtonText: String, proceedAction: @escaping () async -> Void, closeAction: @escaping () -> Void) {
self.image = image
self.title = title
self.description = description
Expand Down
6 changes: 5 additions & 1 deletion DuckDuckGo/HomePage/View/PromotionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ extension HomePage.Views {

private var button: some View {
Group {
Button(action: viewModel.proceedAction) {
Button {
Task { @MainActor in
await viewModel.proceedAction()
}
} label: {
Text(verbatim: viewModel.proceedButtonText)
}
.controlSize(.large)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// DefaultHomePageSettingsModelNavigator+NewTabPageLinkOpening.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 NewTabPage

extension DefaultHomePageSettingsModelNavigator: NewTabPageLinkOpening {

func openLink(_ target: NewTabPageDataModel.OpenAction.Target) async {
switch target {
case .settings:
openAppearanceSettings()
}
}
}
11 changes: 10 additions & 1 deletion DuckDuckGo/NewTabPage/NewTabPageActionsManagerExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,18 @@ extension NewTabPageActionsManager {
getLegacyIsViewExpandedSetting: UserDefaultsWrapper<Bool>(key: .homePageShowAllFavorites, defaultValue: false).wrappedValue
)

let customizationProvider = NewTabPageCustomizationProvider(homePageSettingsModel: NSApp.delegateTyped.homePageSettingsModel)
let freemiumDBPBannerProvider = NewTabPageFreemiumDBPBannerProvider(model: NSApp.delegateTyped.freemiumDBPPromotionViewCoordinator)

self.init(scriptClients: [
NewTabPageConfigurationClient(sectionsVisibilityProvider: appearancePreferences),
NewTabPageConfigurationClient(
sectionsVisibilityProvider: appearancePreferences,
customBackgroundProvider: customizationProvider,
linkOpener: DefaultHomePageSettingsModelNavigator()
),
NewTabPageCustomBackgroundClient(model: customizationProvider),
NewTabPageRMFClient(remoteMessageProvider: activeRemoteMessageModel),
NewTabPageFreemiumDBPClient(provider: freemiumDBPBannerProvider),
NewTabPageNextStepsCardsClient(model: NewTabPageNextStepsCardsProvider(continueSetUpModel: HomePage.Models.ContinueSetUpModel(tabOpener: NewTabPageTabOpener()))),
NewTabPageFavoritesClient(favoritesModel: favoritesModel, preferredFaviconSize: Int(Favicon.SizeCategory.medium.rawValue)),
NewTabPagePrivacyStatsClient(model: privacyStatsModel)
Expand Down
Loading

0 comments on commit e961a41

Please sign in to comment.