Skip to content

Commit

Permalink
Add blue dot indicator to menu item
Browse files Browse the repository at this point in the history
  • Loading branch information
jotaemepereira committed Jan 29, 2025
1 parent 2357cba commit f64d4c1
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 15 deletions.
6 changes: 6 additions & 0 deletions DuckDuckGo-macOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2876,6 +2876,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 */; };
BB1A43902D4968F2000807C7 /* MenuItemWithNotificationDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1A438F2D4968F2000807C7 /* MenuItemWithNotificationDot.swift */; };
BB1A43912D4968F2000807C7 /* MenuItemWithNotificationDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB1A438F2D4968F2000807C7 /* MenuItemWithNotificationDot.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 */; };
Expand Down Expand Up @@ -4876,6 +4878,7 @@
B6FA893E269C424500588ECD /* PrivacyDashboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardViewController.swift; sourceTree = "<group>"; };
B6FA8940269C425400588ECD /* PrivacyDashboardPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacyDashboardPopover.swift; sourceTree = "<group>"; };
BB0346F42CEB80B400D23E05 /* DownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTests.swift; sourceTree = "<group>"; };
BB1A438F2D4968F2000807C7 /* MenuItemWithNotificationDot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItemWithNotificationDot.swift; sourceTree = "<group>"; };
BB3229042D08643700DA92E9 /* TabBarRemoteMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageView.swift; sourceTree = "<group>"; };
BB4339DA2C7F9606005D7ED7 /* PinnedTabsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedTabsTests.swift; sourceTree = "<group>"; };
BB470EBA2C5A66D6002EE91D /* BookmarkManagementDetailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkManagementDetailViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -8428,6 +8431,7 @@
AA86491624D8339A001BABEE /* View */ = {
isa = PBXGroup;
children = (
BB1A438F2D4968F2000807C7 /* MenuItemWithNotificationDot.swift */,
BBB9314C2D1F0F1700D50AC1 /* ShowToolbarsOnFullScreenMenuCoordinator.swift */,
AA7EB6EE27E880EA00036718 /* Animations */,
AAC5E4F025D6BF10007F5990 /* AddressBarButton.swift */,
Expand Down Expand Up @@ -11966,6 +11970,7 @@
B62B483A2ADE46FC000DECE5 /* Application.swift in Sources */,
3706FBEE293F65D500E42796 /* MainView.swift in Sources */,
3706FBEF293F65D500E42796 /* EmailUrlExtensions.swift in Sources */,
BB1A43912D4968F2000807C7 /* MenuItemWithNotificationDot.swift in Sources */,
3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */,
3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */,
1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */,
Expand Down Expand Up @@ -13170,6 +13175,7 @@
37878E562CA3330300CC9EB5 /* HomePageAddressBarModel.swift in Sources */,
85480FCF25D1AA22009424E3 /* ConfigurationStore.swift in Sources */,
AA3D531B27A2F57E00074EC1 /* Feedback.swift in Sources */,
BB1A43902D4968F2000807C7 /* MenuItemWithNotificationDot.swift in Sources */,
4B0A63E8289DB58E00378EF7 /* FirefoxFaviconsReader.swift in Sources */,
1E7E2E9029029A2A00C01B54 /* ContentBlockingRulesUpdateObserver.swift in Sources */,
4B8AC93926B48A5100879451 /* FirefoxLoginReader.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions DuckDuckGo/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {

#if SPARKLE
var updateController: UpdateController!
var dockCustomization: DockCustomization!
#endif

@UserDefaultsWrapper(key: .firstLaunchDate, defaultValue: Date.monthAgo)
Expand Down Expand Up @@ -349,6 +350,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
#if SPARKLE
if NSApp.runType != .uiTests {
updateController = UpdateController(internalUserDecider: internalUserDecider)
dockCustomization = DockCustomizer()
stateRestorationManager.subscribeToAutomaticAppRelaunching(using: updateController.willRelaunchAppPublisher)
}
#endif
Expand Down
27 changes: 26 additions & 1 deletion DuckDuckGo/Application/DockCustomizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,39 @@
//

import Foundation
import Combine
import Common
import os.log
import Persistence

protocol DockCustomization {
var isAddedToDock: Bool { get }
var wasFeatureShownFromMoreOptionsMenu: Bool { get set }
var wasFeatureShownPublisher: AnyPublisher<Bool, Never> { get }

@discardableResult
func addToDock() -> Bool
}

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

private let positionProvider: DockPositionProviding
private let keyValueStore: KeyValueStoring

@Published private var isFeatureShownFromMoreOptionsMenu: Bool = false
var wasFeatureShownPublisher: AnyPublisher<Bool, Never> {
$isFeatureShownFromMoreOptionsMenu.eraseToAnyPublisher()
}

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

isFeatureShownFromMoreOptionsMenu = keyValueStore.object(forKey: Keys.wasAddToDockFeatureShown) as? Bool ?? false
}

private var dockPlistURL: URL = URL(fileURLWithPath: NSString(string: "~/Library/Preferences/com.apple.dock.plist").expandingTildeInPath)
Expand All @@ -53,6 +70,14 @@ final class DockCustomizer: DockCustomization {
return persistentApps.contains(where: { ($0["tile-data"] as? [String: AnyObject])?["bundle-identifier"] as? String == bundleIdentifier })
}

var wasFeatureShownFromMoreOptionsMenu: Bool {
get { return isFeatureShownFromMoreOptionsMenu }
set {
isFeatureShownFromMoreOptionsMenu = newValue
keyValueStore.set(newValue, forKey: Keys.wasAddToDockFeatureShown)
}
}

// 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xDE",
"green" : "0x78",
"red" : "0x64"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xA1",
"green" : "0x44",
"red" : "0x2D"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
1 change: 1 addition & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -684,6 +684,7 @@ final class MainMenu: NSMenu {
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))
NSMenuItem(title: "Reset Add To Dock more options menu notification", action: #selector(MainViewController.resetAddToDockFeatureNotification))

}.withAccessibilityIdentifier("MainMenu.resetData")
NSMenuItem(title: "UI Triggers") {
Expand Down
5 changes: 5 additions & 0 deletions DuckDuckGo/Menus/MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,11 @@ extension MainViewController {
SyncPromoManager().resetPromos()
}

@objc func resetAddToDockFeatureNotification(_ sender: Any?) {
guard var dockCustomizer = Application.appDelegate.dockCustomization else { return }

Check failure on line 916 in DuckDuckGo/Menus/MainMenuActions.swift

View workflow job for this annotation

GitHub Actions / Test (Sandbox)

value of type 'AppDelegate' has no member 'dockCustomization'
dockCustomizer.wasFeatureShownFromMoreOptionsMenu = false
}

@objc func resetTipKit(_ sender: Any?) {
TipKitDebugOptionsUIActionHandler().resetTipKitTapped()
}
Expand Down
57 changes: 57 additions & 0 deletions DuckDuckGo/NavigationBar/View/MenuItemWithNotificationDot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// MenuItemWithNotificationDot.swift
//
// Copyright © 2025 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

/// View that represents a menu item that has a blue notification dot at the right.
struct MenuItemWithNotificationDot: View {
let leftImage: NSImage
let title: String
var onTapMenuItem: () -> Void

@State private var isHovered: Bool = false

var body: some View {
HStack(spacing: 0) {
Image(nsImage: leftImage)
.resizable()
.foregroundColor(isHovered ? .white : .blackWhite100)
.frame(width: 16, height: 16)
.padding([.leading, .trailing], 6)

Text(title)
.foregroundColor(isHovered ? .white : .blackWhite100.opacity(0.9))
.frame(maxWidth: .infinity, alignment: .leading)

Circle()
.fill(isHovered ? .white : .updateIndicator)
.frame(width: 7, height: 7)
.padding(.trailing, 6)
}
.padding(4)
.background(isHovered ? .menuItemHover : Color.clear)
.cornerRadius(5)
.onHover { hovering in
isHovered = hovering
}
.padding(4)
.onTapGesture {
onTapMenuItem()
}
}
}
49 changes: 40 additions & 9 deletions DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import Subscription
import os.log
import Freemium
import DataBrokerProtection
import SwiftUI

protocol OptionsButtonMenuDelegate: AnyObject {

Expand Down Expand Up @@ -65,7 +66,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
private let freemiumDBPFeature: FreemiumDBPFeature
private let freemiumDBPPresenter: FreemiumDBPPresenter
private let appearancePreferences: AppearancePreferences
private let dockCustomizer: DockCustomization
private var dockCustomizer: DockCustomization?
private let defaultBrowserPreferences: DefaultBrowserPreferences

private let notificationCenter: NotificationCenter
Expand Down Expand Up @@ -94,7 +95,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
freemiumDBPFeature: FreemiumDBPFeature,
freemiumDBPPresenter: FreemiumDBPPresenter = DefaultFreemiumDBPPresenter(),
appearancePreferences: AppearancePreferences = .shared,
dockCustomizer: DockCustomization = DockCustomizer(),
dockCustomizer: DockCustomization? = nil,
defaultBrowserPreferences: DefaultBrowserPreferences = .shared,
notificationCenter: NotificationCenter = .default,
freemiumDBPExperimentPixelHandler: EventMapping<FreemiumDBPExperimentPixel> = FreemiumDBPExperimentPixelHandler(),
Expand Down Expand Up @@ -153,12 +154,28 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {

#endif // FEEDBACK

#if !APPSTORE
if !dockCustomizer.isAddedToDock {
let addToDockMenuItem = NSMenuItem(title: UserText.addDuckDuckGoToDock, action: #selector(addToDock(_:)))
.targetting(self)
.withImage(.addToDockMenuItem)
addItem(addToDockMenuItem)
#if SPARKLE
if let dockCustomizer = self.dockCustomizer {
if dockCustomizer.isAddedToDock == false {
if dockCustomizer.wasFeatureShownFromMoreOptionsMenu {
let addToDockMenuItem = NSMenuItem(title: UserText.addDuckDuckGoToDock, action: #selector(addToDock(_:)))
.targetting(self)
.withImage(.addToDockMenuItem)
addItem(addToDockMenuItem)
} else {
let addToDockMenuItem = NSMenuItem(action: #selector(addToDock(_:)))
.targetting(self)
addToDockMenuItem.view = createMenuItemWithFeatureIndicator(
title: UserText.addDuckDuckGoToDock,
image: .addToDockMenuItem) {
if let target = addToDockMenuItem.target {
_ = target.perform(addToDockMenuItem.action, with: addToDockMenuItem)
// TODO: Need to close the menu when this happens

Check failure on line 173 in DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

TODOs should be resolved (Need to close the menu when th...) (todo)
}
}
addItem(addToDockMenuItem)
}
}
}
#endif
if !defaultBrowserPreferences.isDefault {
Expand Down Expand Up @@ -199,6 +216,16 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
addItem(preferencesItem)
}

private func createMenuItemWithFeatureIndicator(title: String, image: NSImage, onTap: @escaping () -> Void) -> NSView {
let menuItem = MenuItemWithNotificationDot(leftImage: image, title: title, onTapMenuItem: onTap)

let hostingView = NSHostingView(rootView: menuItem)
hostingView.frame = NSRect(x: 0, y: 0, width: size.width, height: 22)
hostingView.autoresizingMask = [.width, .height]

return hostingView
}

@objc func openDataBrokerProtection(_ sender: NSMenuItem) {
actionDelegate?.optionsButtonMenuRequestedDataBrokerProtection(self)
}
Expand All @@ -210,7 +237,7 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
@MainActor
@objc func addToDock(_ sender: NSMenuItem) {
PixelKit.fire(GeneralPixel.userAddedToDockFromMoreOptionsMenu)
dockCustomizer.addToDock()
dockCustomizer?.addToDock()
}

@MainActor
Expand Down Expand Up @@ -530,6 +557,10 @@ final class MoreOptionsMenu: NSMenu, NSMenuDelegate {
}
#endif
}

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

final class EmailOptionsButtonSubMenu: NSMenu {
Expand Down
10 changes: 6 additions & 4 deletions DuckDuckGo/NavigationBar/View/MoreOptionsMenuButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class MoreOptionsMenuButton: MouseOverButton {

#if SPARKLE
private var updateController: UpdateControllerProtocol?
private var dockCustomization: DockCustomization?
#endif

private var notificationLayer: CALayer?
Expand All @@ -52,6 +53,7 @@ final class MoreOptionsMenuButton: MouseOverButton {
#if SPARKLE
if NSApp.runType != .uiTests {
updateController = Application.appDelegate.updateController
dockCustomization = Application.appDelegate.dockCustomization
}
subscribeToUpdateInfo()
#endif
Expand All @@ -64,11 +66,11 @@ final class MoreOptionsMenuButton: MouseOverButton {

private func subscribeToUpdateInfo() {
#if SPARKLE
guard let updateController else { return }
cancellable = Publishers.CombineLatest(updateController.hasPendingUpdatePublisher, updateController.notificationDotPublisher)
guard let updateController, let dockCustomization else { return }
cancellable = Publishers.CombineLatest3(updateController.hasPendingUpdatePublisher, updateController.notificationDotPublisher, dockCustomization.wasFeatureShownPublisher)
.receive(on: DispatchQueue.main)
.sink { [weak self] hasPendingUpdate, needsNotificationDot in
self?.isNotificationVisible = hasPendingUpdate && needsNotificationDot
.sink { [weak self] hasPendingUpdate, needsNotificationDot, wasAddToDockFeatureShown in
self?.isNotificationVisible = hasPendingUpdate && needsNotificationDot || !wasAddToDockFeatureShown
}
#endif
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,17 @@ final class NavigationBarViewController: NSViewController {
@IBAction func optionsButtonAction(_ sender: NSButton) {
let internalUserDecider = NSApp.delegateTyped.internalUserDecider
let freemiumDBPFeature = Application.appDelegate.freemiumDBPFeature
var dockCustomization: DockCustomization? = nil

Check failure on line 307 in DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift

View workflow job for this annotation

GitHub Actions / SwiftLint

Initializing an optional variable with nil is redundant (redundant_optional_initialization)
#if SPARKLE
dockCustomization = Application.appDelegate.dockCustomization
#endif
let menu = MoreOptionsMenu(tabCollectionViewModel: tabCollectionViewModel,
passwordManagerCoordinator: PasswordManagerCoordinator.shared,
vpnFeatureGatekeeper: DefaultVPNFeatureGatekeeper(subscriptionManager: subscriptionManager),
internalUserDecider: internalUserDecider,
subscriptionManager: subscriptionManager,
freemiumDBPFeature: freemiumDBPFeature)
freemiumDBPFeature: freemiumDBPFeature,
dockCustomizer: dockCustomization)

menu.actionDelegate = self
let location = NSPoint(x: -menu.size.width + sender.bounds.width, y: sender.bounds.height + 4)
Expand Down
Loading

0 comments on commit f64d4c1

Please sign in to comment.