Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions client/ui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"github.com/sirupsen/logrus"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/events"
"github.com/wailsapp/wails/v3/pkg/services/notifications"

"github.com/netbirdio/netbird/client/ui/authsession"
"github.com/netbirdio/netbird/client/ui/i18n"
Expand Down Expand Up @@ -63,7 +62,7 @@ type registeredServices struct {
profiles *services.Profiles
update *services.Update
daemonFeed *services.DaemonFeed
notifier *notifications.NotificationService
notifier *Notifier
compat *services.Compat
profileSwitcher *services.ProfileSwitcher
bundle *i18n.Bundle
Expand Down Expand Up @@ -103,7 +102,7 @@ func main() {
updaterHolder := updater.NewHolder(app.Event)
update := services.NewUpdate(conn, updaterHolder)
daemonFeed := services.NewDaemonFeed(conn, app.Event, updaterHolder, debugLog)
notifier := notifications.New()
notifier := newNotifier()
compat := services.NewCompat(conn)
// macOS shows no toast until permission is requested. Run it after
// ApplicationStarted so the notifier's Startup has initialised the
Expand Down Expand Up @@ -210,7 +209,7 @@ func main() {
// requestNotificationAuthorization prompts for macOS notification permission.
// The request blocks until the user responds (up to 3 minutes), so callers run
// it in a goroutine. No-op on Linux/Windows.
func requestNotificationAuthorization(notifier *notifications.NotificationService) {
func requestNotificationAuthorization(notifier *Notifier) {
authorized, err := notifier.CheckNotificationAuthorization()
if err != nil {
logrus.Debugf("check notification authorization: %v", err)
Expand Down
101 changes: 101 additions & 0 deletions client/ui/notifier.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//go:build !android && !ios && !freebsd && !js

package main

import (
"context"
"errors"
"sync/atomic"

log "github.com/sirupsen/logrus"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/wailsapp/wails/v3/pkg/services/notifications"
)

var errNotificationsUnavailable = errors.New("notifications unavailable")

// Notifier wraps the Wails notification service so an unavailable backend
// disables notifications instead of aborting the app. Startup fails for
// environment reasons (a bare unbundled binary on macOS has no bundle
// identifier, a headless Linux session has no D-Bus session bus), and Wails
// treats a service startup error as fatal. After a failed startup every call
// is a no-op: on macOS, touching UNUserNotificationCenter without a bundle
// identifier raises an Objective-C exception that recover() cannot catch.
type Notifier struct {
inner *notifications.NotificationService
available atomic.Bool
}

func newNotifier() *Notifier {
return &Notifier{inner: notifications.New()}
}

// ServiceName implements the Wails service-name hook for startup logs.
func (n *Notifier) ServiceName() string {
return n.inner.ServiceName()
}

// ServiceStartup starts the platform notifier, downgrading failure to a
// warning so the app keeps running without notifications.
func (n *Notifier) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if err := n.inner.ServiceStartup(ctx, options); err != nil {
log.Warnf("notifications disabled: %v", err)
return nil
}
n.available.Store(true)
return nil
}

func (n *Notifier) ServiceShutdown() error {
if !n.available.Load() {
return nil
}
return n.inner.ServiceShutdown()
}

func (n *Notifier) CheckNotificationAuthorization() (bool, error) {
if !n.available.Load() {
return false, errNotificationsUnavailable
}
return n.inner.CheckNotificationAuthorization()
}

func (n *Notifier) RequestNotificationAuthorization() (bool, error) {
if !n.available.Load() {
return false, errNotificationsUnavailable
}
return n.inner.RequestNotificationAuthorization()
}

// SendNotification delivers a notification, silently dropping it when the
// backend never started (notifications are best-effort everywhere).
func (n *Notifier) SendNotification(options notifications.NotificationOptions) error {
if !n.available.Load() {
log.Debugf("notifications disabled, dropping %q", options.ID)
return nil
}
return n.inner.SendNotification(options)
}

func (n *Notifier) SendNotificationWithActions(options notifications.NotificationOptions) error {
if !n.available.Load() {
log.Debugf("notifications disabled, dropping %q", options.ID)
return nil
}
return n.inner.SendNotificationWithActions(options)
}

func (n *Notifier) RegisterNotificationCategory(category notifications.NotificationCategory) error {
if !n.available.Load() {
return nil
}
return n.inner.RegisterNotificationCategory(category)
Comment on lines +56 to +92

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/sh
rg -n 'UpdateNotification|RemoveNotification(Category)?|Remove(All)?(Pending|Delivered)Notifications?' .

Repository: netbirdio/netbird

Length of output: 965


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== files =="
git ls-files | rg '^(client/ui/(notifier|main|.*notifications).*\.go|go\.mod|go\.sum)$|notifications' | head -200

echo
echo "== Locate NotificationService definitions/usages =="
rg -n "NotificationService|type Notify|func .*Notification|RegisterNotificationCategory|requestBackend|available" client/ui shared -S

echo
echo "== notifier.go outline =="
ast-grep outline client/ui/notifier.go --view expanded || true

echo
echo "== notifier.go relevant section =="
cat -n client/ui/notifier.go | sed -n '1,160p'

echo
echo "== main.go registration context =="
cat -n client/ui/main.go | sed -n '300,350p'

echo
echo "== package notification files =="
for f in $(git ls-files | rg 'notifications\.go$|.*notifications.*\.go$' | grep -E 'client/(ui|.*backend|internal)|.*notifications.*'); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,240p' "$f"
done

Repository: netbirdio/netbird

Length of output: 39965


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== go.mod wails dependency =="
rg -n 'github\.com/wailsapp/wails|v3' go.mod go.sum | head -80

echo
echo "== inspect Wails module cache if present =="
MODCACHE="${GOPATH:-$HOME/go}/pkg/cache/modules"
if [ -d "$MODCACHE" ]; then
  fd 'notifications\.go$|notifications.*\.go$' "$MODCACHE/github.com/wailsapp/wails" | head -50 || true
  for f in $(fd 'notifications\.go$|notifications.*\.go$' "$MODCACHE/github.com/wailsapp/wails" | head -50); do
    echo "--- $f"
    wc -l "$f"
    rg -n 'type NotificationService|func .*Notification|UpdateNotification|RemoveNotification|Clear|All|Pending|Delivered|NotificationCategory|NotificationResult|NotificationResponse|ServiceStartup' "$f" -C 2
  done
else
  echo "no wails module cache found at $MODCACHE"
fi

Repository: netbirdio/netbird

Length of output: 5010


🌐 Web query:

github.com wailsapp wails v3.0.0-alpha2.117 notifications Service NotificationService UpdateNotification RemoveNotification

💡 Result:

In Wails v3 (including version v3.0.0-alpha2.117), the NotificationService is part of the github.com/wailsapp/wails/v3/pkg/services/notifications package [1][2][3]. This service provides a cross-platform interface for managing system notifications [2]. The requested methods function as follows: - UpdateNotification: This method updates an existing, in-flight notification using its unique ID [2][4]. When calling this method, you provide NotificationOptions containing the updated content [2]. On platforms like macOS and Linux, this typically replaces the notification in place, while on Windows, it may redeliver the notification [4]. - RemoveNotification: The NotificationService supports removing notifications, generally identified by the unique ID provided in the original notification options [2][5]. While specific API availability can vary by platform—for instance, macOS offers more granular control through UNUserNotificationCenter methods like RemoveDeliveredNotification and RemovePendingNotification—the NotificationService abstraction is designed to handle notification lifecycle management [2][4][5]. To use the service, you initialize it via notifications.New() and register it within your application's Service options [2][6]: import "github.com/wailsapp/wails/v3/pkg/application" import "github.com/wailsapp/wails/v3/pkg/services/notifications" // Create the service notifier:= notifications.New // Register the service with the application app:= application.New(application.Options{ Services: []application.Service{ application.NewService(notifier), }, }) For detailed implementation, refer to the official Wails v3 documentation [2][7] or the source code in the v3/pkg/services/notifications directory [1][3].

Citations:


🌐 Web query:

pkg.go.dev github.com/wailsapp/wails v3/pkg/services/notifications NotificationService

💡 Result:

The NotificationService in the Wails v3 framework is a cross-platform service located in the github.com/wailsapp/wails/v3/pkg/services/notifications package [1][2]. It is designed to provide desktop applications with the ability to display native system notifications, including basic alerts, interactive notifications with actions, and categorized notification configurations [3][2]. Key aspects of the NotificationService include: Integration: To use the service, you typically create an instance using notifications.New and register it with your Wails application during initialization within the application.Options struct [3][4]. Functional Capabilities: The service supports methods such as SendNotification and SendNotificationWithActions, allowing developers to handle notifications consistently across macOS, Windows, and Linux [3][5][2]. Callback System: NotificationService provides an OnNotificationResponse method, which registers a callback function. This function is invoked when a user interacts with a notification (e.g., clicking a button or responding), allowing the application to process the resulting NotificationResult [1][4][5]. Internal Implementation: The service uses a platform-specific implementation (darwin, linux, or windows) under the hood [6][5]. These implementations manage the platform's native notification APIs and thread-safely bridge interaction events back to the main service, which then triggers the registered user callback [1][6][5]. For detailed usage, developers are encouraged to consult the official Wails v3 documentation [3] or the package documentation on pkg.go.dev [2].

Citations:


🌐 Web query:

github.com wailsapp wails v3.0.0-alpha2.117 notifications.go UpdateNotification RemoveNotification RemoveAllPendingNotifications RemoveAllDeliveredNotifications

💡 Result:

In Wails v3.0.0-alpha2.117, the notification management methods you identified—UpdateNotification, RemoveNotification, RemoveAllPendingNotifications, and RemoveAllDeliveredNotifications—are part of the cross-platform notifications service defined in github.com/wailsapp/wails/v3/pkg/services/notifications [1][2]. Key details regarding these methods include: UpdateNotification: This method is used to update an existing "in-flight" notification by its ID [3]. RemoveNotification: In the macOS implementation (notifications_darwin.go), this method exists as a stub that returns nil [4]. The documentation explicitly advises against using it, directing developers to use more specific methods for pending or delivered notifications instead [4]. RemoveAllPendingNotifications and RemoveAllDeliveredNotifications: These are specific management methods for removing notifications from their respective states (pending or delivered) [4]. General Usage: These methods are part of the NotificationService struct [1][2]. Because Wails v3 supports different operating systems (macOS, Windows, and Linux), the underlying implementation handles platform-specific differences [1][2]. For instance, the macOS implementation provides concrete functions for removing notifications by state (pending or delivered) [4]. Note: Ensure you are referencing the v3-specific documentation at https://v3.wails.io/features/notifications/overview/ rather than the v2 runtime documentation, as the API structure differs between versions [5][3][6].

Citations:


🌐 Web query:

github.com wailsapp/wails/blob/3ec88c12/v3/pkg/services/notifications/notifications.go

💡 Result:

The file located at v3/pkg/services/notifications/notifications.go is part of the Wails v3 framework, which provides a cross-platform service for displaying native system notifications [1][2][3]. Key characteristics and functionality of this package include: Cross-Platform Support: It supports macOS, Windows, and Linux by abstracting platform-specific differences while exposing a unified Go API [1][2]. Core Functionality: - Basic notifications: Supports titles, subtitles, and body text [1][4]. - Interactive notifications: Allows defining categories with actionable buttons [1][2]. - Callback System: Provides a unified response system via OnNotificationResponse to handle user interaction with notifications [2][5]. - Configuration: Uses a NotificationOptions struct for flexible configuration, including unique IDs, data payloads, and interruption levels [1][4]. Usage: To use the service, you must instantiate it (notifications.New) and register it with the Wails application during setup [4][5]. Note that for platform-specific features (such as on macOS), applications may need to be correctly packaged and signed to function as expected [6]. The package path has been standardized to github.com/wailsapp/wails/v3/pkg/services/notifications in recent versions to ensure correct compilation [7].

Citations:


🏁 Script executed:

#!/bin/sh
set -euo pipefail

echo "== raw remote source sections for pinned tags/commits =="
for ref in "3ec88c12" "v3.0.0-alpha2.117"; do
  echo "--- reference $ref"
  base="https://raw.githubusercontent.com/wailsapp/wails/$ref/v3/pkg/services/notifications"
  for f in notifications.go notifications_darwin.go notifications_linux.go notifications_windows.go; do
    url="$base/$f"
    if command -v curl >/dev/null 2>&1; then
      echo "//// curl $url"
      curl -sS "$url" | rg -n 'type NotificationService|func .*Notification|UpdateNotification|RemoveNotification|RemoveAll(Pending|Delivered)|RegisterNotificationCategory|SendNotification|NotificationResult|NotificationResponse' -C 2 || true
    fi
  done
done

Repository: netbirdio/netbird

Length of output: 44794


Preserve the full Wails notification service API on Notifier.

*Notifier now replaces notifications.NotificationService at client/ui/main.go:330, but it only forwards a subset of methods. Wails NotificationService also exposes UpdateNotification, RemoveNotificationCategory, RemoveAllPendingNotifications, RemovePendingNotification, RemoveAllDeliveredNotifications, RemoveDeliveredNotification, and RemoveNotification; omitting these changes the generated frontend contract. Add availability-gated forwarding for the full surface, or treat this change as intentional and update callers/frontend-generated APIs accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/ui/notifier.go` around lines 56 - 92, The Notifier wrapper does not
preserve the complete notifications.NotificationService API. Extend *Notifier
with availability-gated forwarding methods for UpdateNotification,
RemoveNotificationCategory, RemoveAllPendingNotifications,
RemovePendingNotification, RemoveAllDeliveredNotifications,
RemoveDeliveredNotification, and RemoveNotification, matching the underlying
service signatures and existing unavailable behavior; keep the current methods
unchanged.

Source: MCP tools

}

// OnNotificationResponse registers the response callback. Pure Go state, so
// it is safe (and simply inert) when the backend never started.
//
//wails:ignore
func (n *Notifier) OnNotificationResponse(callback func(result notifications.NotificationResult)) {
n.inner.OnNotificationResponse(callback)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
2 changes: 1 addition & 1 deletion client/ui/tray.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type TrayServices struct {
Profiles *services.Profiles
Networks *services.Networks
DaemonFeed *services.DaemonFeed
Notifier *notifications.NotificationService
Notifier *Notifier
Update *services.Update
ProfileSwitcher *services.ProfileSwitcher
WindowManager *services.WindowManager
Expand Down
2 changes: 1 addition & 1 deletion client/ui/tray_notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func safeSendNotification(send sendFn, what string, opts notifications.Notificat
// notifyIfDaemonOutdated probes the daemon once and fires an OS toast when it
// is reachable but too old for this UI. A probe error means the daemon isn't
// reachable (not outdated), so it is left to the normal connection flow.
func notifyIfDaemonOutdated(compat *services.Compat, notifier *notifications.NotificationService, loc *Localizer) {
func notifyIfDaemonOutdated(compat *services.Compat, notifier *Notifier, loc *Localizer) {
ready, err := compat.DaemonReady(context.Background())
if err != nil {
log.Debugf("daemon compatibility probe: %v", err)
Expand Down
4 changes: 2 additions & 2 deletions client/ui/tray_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type trayUpdater struct {
app *application.App
window *application.WebviewWindow
update *services.Update
notifier *notifications.NotificationService
notifier *Notifier
loc *Localizer
onIconChange func()
// onMenuChange drives a full tray relayout: the update row lives in the
Expand All @@ -36,7 +36,7 @@ type trayUpdater struct {
progressWindowOpen bool
}

func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *notifications.NotificationService, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater {
func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *Notifier, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater {
u := &trayUpdater{
app: app,
window: window,
Expand Down
Loading