Skip to content

Commit

Permalink
Implement blue-dot new business logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira committed Jan 30, 2025
1 parent 2d8fa5d commit 8b04bad
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 28 deletions.
4 changes: 4 additions & 0 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
return firstLaunchDate >= Date.weekAgo
}

static var twoDaysPassedSinceFirstLaunch: Bool {
return firstLaunchDate.daysSinceNow() >= 2
}

@MainActor
override init() {
// will not add crash handlers and will fire pixel on applicationDidFinishLaunching if didCrashDuringCrashHandlersSetUp == true
Expand Down
59 changes: 47 additions & 12 deletions DuckDuckGo/Application/DockCustomizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,43 @@ import Persistence

protocol DockCustomization {
var isAddedToDock: Bool { get }
var didShowFeatureFromMoreOptionsMenu: Bool { get set }
var didShowPublisher: AnyPublisher<Bool, Never> { get }

@discardableResult
func addToDock() -> Bool

/// The notification mentiond here is the blue dot notification shown in the more options menu.
/// The blue dot is also show in the Add To Dock menu item.
///
/// The requriments for the blue dot show to shown are the following:
/// - Two days passed since first lauch.
/// - We didn't show it in the past (this means the blue dot was shown, the user opened the more options menu and then closed it)
var shouldShowNotification: Bool { get }
var shouldShowNotificationPublisher: AnyPublisher<Bool, Never> { get }
func didCloseMoreOptionsMenu()
func resetData()
}

final class DockCustomizer: DockCustomization {
enum Keys {
static let wasAddToDockFeatureShown = "more-options-menu.was-add-to-dock-shown"
static let wasNotificationShownToUser = "was-dock-notification.show-to-users"
}

private let positionProvider: DockPositionProviding
private let keyValueStore: KeyValueStoring

@Published private var didShowFeatureFromMoreOptionsMenuPrivate: Bool = false
var didShowPublisher: AnyPublisher<Bool, Never> {
$didShowFeatureFromMoreOptionsMenuPrivate.eraseToAnyPublisher()
@Published private var shouldShowNotificationPrivate: Bool = false
var shouldShowNotificationPublisher: AnyPublisher<Bool, Never> {
$shouldShowNotificationPrivate.eraseToAnyPublisher()
}
private var cancellables = Set<AnyCancellable>()

init(positionProvider: DockPositionProviding = DockPositionProvider(),
keyValueStore: KeyValueStoring = UserDefaults.standard) {
self.positionProvider = positionProvider
self.keyValueStore = keyValueStore

didShowFeatureFromMoreOptionsMenuPrivate = keyValueStore.object(forKey: Keys.wasAddToDockFeatureShown) as? Bool ?? false
shouldShowNotificationPrivate = shouldShowNotification
startTimer()
}

private var dockPlistURL: URL = URL(fileURLWithPath: NSString(string: "~/Library/Preferences/com.apple.dock.plist").expandingTildeInPath)
Expand All @@ -58,6 +69,26 @@ final class DockCustomizer: DockCustomization {
return NSDictionary(contentsOf: dockPlistURL) as? [String: AnyObject]
}

private func startTimer() {
// We will check every 12 hours: 12 * 60 * 60
Timer.publish(every: 60, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
guard let self = self else { return }

self.shouldShowNotificationPrivate = self.shouldShowNotification
}
.store(in: &cancellables)
}

private var didWeShowNotificationToUser: Bool {
keyValueStore.object(forKey: Keys.wasNotificationShownToUser) as? Bool ?? false
}

var shouldShowNotification: Bool {
AppDelegate.twoDaysPassedSinceFirstLaunch && !didWeShowNotificationToUser
}

// This checks whether the bundle identifier of the current bundle
// is present in the 'persistent-apps' array of the Dock's plist.
var isAddedToDock: Bool {
Expand All @@ -70,14 +101,18 @@ final class DockCustomizer: DockCustomization {
return persistentApps.contains(where: { ($0["tile-data"] as? [String: AnyObject])?["bundle-identifier"] as? String == bundleIdentifier })
}

var didShowFeatureFromMoreOptionsMenu: Bool {
get { return didShowFeatureFromMoreOptionsMenuPrivate }
set {
didShowFeatureFromMoreOptionsMenuPrivate = newValue
keyValueStore.set(newValue, forKey: Keys.wasAddToDockFeatureShown)
func didCloseMoreOptionsMenu() {
if AppDelegate.twoDaysPassedSinceFirstLaunch {
shouldShowNotificationPrivate = false
keyValueStore.set(true, forKey: Keys.wasNotificationShownToUser)
}
}

func resetData() {
keyValueStore.set(false, forKey: Keys.wasNotificationShownToUser)
shouldShowNotificationPrivate = shouldShowNotification
}

// Adds a dictionary representing the application, either by using an existing
// one from 'recent-apps' or creating a new one if the application isn't recently used.
// It then inserts this dictionary into the 'persistent-apps' list at a position
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,8 @@ final class MainMenu: NSMenu {
NSMenuItem(title: "Reset Contextual Onboarding", action: #selector(MainViewController.resetContextualOnboarding(_:)))
NSMenuItem(title: "Reset Sync Promo prompts", action: #selector(MainViewController.resetSyncPromoPrompts))
NSMenuItem(title: "Reset Add To Dock more options menu notification", action: #selector(MainViewController.resetAddToDockFeatureNotification))
NSMenuItem(title: "Reset Launch Date To Today", action: #selector(MainViewController.resetLaunchDateToToday))
NSMenuItem(title: "Set Launch Date A Week In the Past", action: #selector(MainViewController.setLaunchDayAWeekInThePast))

}.withAccessibilityIdentifier("MainMenu.resetData")
NSMenuItem(title: "UI Triggers") {
Expand Down
12 changes: 10 additions & 2 deletions DuckDuckGo/Menus/MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -914,11 +914,19 @@ extension MainViewController {

@objc func resetAddToDockFeatureNotification(_ sender: Any?) {
#if SPARKLE
guard var dockCustomizer = Application.appDelegate.dockCustomization else { return }
dockCustomizer.didShowFeatureFromMoreOptionsMenu = false
guard let dockCustomizer = Application.appDelegate.dockCustomization else { return }
dockCustomizer.resetData()
#endif
}

@objc func resetLaunchDateToToday(_ sender: Any?) {
UserDefaults.standard.set(Date(), forKey: UserDefaultsWrapper<Any>.Key.firstLaunchDate.rawValue)
}

@objc func setLaunchDayAWeekInThePast(_ sender: Any?) {
UserDefaults.standard.set(Date.weekAgo, forKey: UserDefaultsWrapper<Any>.Key.firstLaunchDate.rawValue)
}

@objc func resetTipKit(_ sender: Any?) {
TipKitDebugOptionsUIActionHandler().resetTipKitTapped()
}
Expand Down
14 changes: 7 additions & 7 deletions DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
#if SPARKLE
if let dockCustomizer = self.dockCustomizer {
if dockCustomizer.isAddedToDock == false {
if dockCustomizer.didShowFeatureFromMoreOptionsMenu {
let addToDockMenuItem = NSMenuItem(title: UserText.addDuckDuckGoToDock, action: #selector(addToDock(_:)))
.targetting(self)
.withImage(.addToDockMenuItem)
addItem(addToDockMenuItem)
} else {
if dockCustomizer.shouldShowNotification {
let addToDockMenuItem = NSMenuItem(action: #selector(addToDock(_:)))
.targetting(self)
addToDockMenuItem.view = createMenuItemWithFeatureIndicator(
Expand All @@ -174,6 +169,11 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
self.cancelTracking()
}
addItem(addToDockMenuItem)
} else {
let addToDockMenuItem = NSMenuItem(title: UserText.addDuckDuckGoToDock, action: #selector(addToDock(_:)))
.targetting(self)
.withImage(.addToDockMenuItem)
addItem(addToDockMenuItem)
}
}
}
Expand Down Expand Up @@ -559,7 +559,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
}

func menuDidClose(_ menu: NSMenu) {
dockCustomizer?.didShowFeatureFromMoreOptionsMenu = true
dockCustomizer?.didCloseMoreOptionsMenu()
}
}

Expand Down
6 changes: 3 additions & 3 deletions DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ final class MoreOptionsMenuButton: MouseOverButton {
private func subscribeToUpdateInfo() {
#if SPARKLE
guard let updateController, let dockCustomization else { return }
cancellable = Publishers.CombineLatest3(updateController.hasPendingUpdatePublisher, updateController.notificationDotPublisher, dockCustomization.didShowPublisher)
cancellable = Publishers.CombineLatest3(updateController.hasPendingUpdatePublisher, updateController.notificationDotPublisher, dockCustomization.shouldShowNotificationPublisher)
.receive(on: DispatchQueue.main)
.sink { [weak self] hasPendingUpdate, needsNotificationDot, wasAddToDockFeatureShown in
self?.isNotificationVisible = hasPendingUpdate && needsNotificationDot || !wasAddToDockFeatureShown
.sink { [weak self] hasPendingUpdate, needsNotificationDot, shouldNotificationForAddToDock in
self?.isNotificationVisible = hasPendingUpdate && needsNotificationDot || shouldNotificationForAddToDock
}
#endif
}
Expand Down
12 changes: 10 additions & 2 deletions UnitTests/App/DockCustomizerMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import XCTest
class DockCustomizerMock: DockCustomization {
private var featureShownSubject = CurrentValueSubject<Bool, Never>(false)

var didShowPublisher: AnyPublisher<Bool, Never> {
var shouldShowNotificationPublisher: AnyPublisher<Bool, Never> {
featureShownSubject.eraseToAnyPublisher()
}

var didShowFeatureFromMoreOptionsMenu: Bool {
var shouldShowNotification: Bool {
get { featureShownSubject.value }
set { featureShownSubject.send(newValue) }
}
Expand All @@ -48,4 +48,12 @@ class DockCustomizerMock: DockCustomization {
return false
}
}

func didCloseMoreOptionsMenu() {
// No-op
}

func resetData() {
// No-op
}
}
12 changes: 10 additions & 2 deletions UnitTests/Onboarding/Mocks/CapturingDockCustomizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ import Foundation
class CapturingDockCustomizer: DockCustomization {
private var featureShownSubject = CurrentValueSubject<Bool, Never>(false)

var didShowPublisher: AnyPublisher<Bool, Never> {
var shouldShowNotificationPublisher: AnyPublisher<Bool, Never> {
featureShownSubject.eraseToAnyPublisher()
}

var didShowFeatureFromMoreOptionsMenu: Bool {
var shouldShowNotification: Bool {
get { featureShownSubject.value }
set { featureShownSubject.send(newValue) }
}
Expand All @@ -38,4 +38,12 @@ class CapturingDockCustomizer: DockCustomization {
isAddedToDock = true
return true
}

func didCloseMoreOptionsMenu() {
// No-op
}

func resetData() {
// No-op
}
}

0 comments on commit 8b04bad

Please sign in to comment.