Files
netbird/client/ui/tray_events.go

126 lines
4.1 KiB
Go

//go:build !android && !ios && !freebsd && !js
package main
import (
"fmt"
"strings"
"github.com/wailsapp/wails/v3/pkg/application"
"github.com/netbirdio/netbird/client/ui/authsession"
"github.com/netbirdio/netbird/client/ui/services"
)
// onSystemEvent fires an OS notification for daemon SystemEvents that carry
// a user-facing message, mirroring the legacy event.Manager behaviour: gated
// by the user's "Notifications" toggle, with CRITICAL events bypassing the
// gate. Update-related events are skipped here because trayUpdater produces
// its own richer notification when EventUpdateState fires.
func (t *Tray) onSystemEvent(ev *application.CustomEvent) {
se, ok := ev.Data.(services.SystemEvent)
if !ok {
return
}
// Session-warning and deadline-rejected events carry no UserMessage —
// the tray builds the localised notification body locally from metadata.
// Every other event needs a non-empty UserMessage to show anything meaningful.
isSessionWarning := se.Metadata[authsession.MetaWarning] == "true"
isDeadlineRejected := se.Metadata[authsession.MetaDeadlineRejected] != ""
if !isSessionWarning && !isDeadlineRejected && se.UserMessage == "" {
return
}
if shouldSkipSystemEvent(se) {
return
}
critical := se.Severity == "critical"
t.profileMu.Lock()
enabled := t.notificationsEnabled
t.profileMu.Unlock()
if !enabled && !critical {
return
}
// Session-warning events come in two flavours; detect via the stable
// metadata flags rather than category/severity so a future reword on
// the daemon side still routes here.
// - T-WarningLead (MetaSessionWarning + no MetaSessionFinal) →
// interactive "Extend now / Dismiss" OS notification. Title and
// body are built locally from i18n + metadata so the text follows
// the active UI language regardless of what the daemon (which has
// no locale context) writes into UserMessage.
// - T-FinalWarningLead (MetaSessionFinal=true) → auto-open the
// SessionExpiration dialog. No OS notification here; the
// dialog is the last-chance reminder, doubling up would be noise.
if isDeadlineRejected {
t.notify(
t.loc.T("notify.sessionDeadlineRejected.title"),
t.loc.T("notify.sessionDeadlineRejected.body"),
notifyIDSessionExpired,
)
return
}
if se.Metadata != nil && se.Metadata[authsession.MetaWarning] == "true" {
if se.Metadata[authsession.MetaFinal] == "true" {
t.openSessionExpiration()
return
}
t.notifySessionWarning(
t.loc.T("notify.sessionWarning.title"),
t.buildSessionWarningBody(se.Metadata),
)
return
}
body := se.UserMessage
if id := se.Metadata["id"]; id != "" {
body += fmt.Sprintf(" ID: %s", id)
}
t.notify(eventTitle(se), body, notifyIDEvent+se.ID)
}
// eventTitle composes a notification title from a SystemEvent's severity and
// category — "Critical: DNS", "Warning: Authentication", etc. — matching the
// format the legacy Fyne event.Manager produced.
func eventTitle(e services.SystemEvent) string {
prefix := titleCase(e.Severity)
if prefix == "" {
prefix = "Info"
}
category := titleCase(e.Category)
if category == "" {
category = "System"
}
return prefix + ": " + category
}
func titleCase(s string) string {
if s == "" {
return ""
}
return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
// shouldSkipSystemEvent reports whether a daemon SystemEvent must not
// surface as a tray notification. Three sources are filtered out:
// - update-available announcements (trayUpdater emits its own richer
// notification when EventUpdateState fires)
// - install-progress signals (consumed by the install-progress window)
// - the ::/0 partner of an exit-node default-route event (the 0.0.0.0/0
// partner already drove the user-facing toast, so the v6 row is
// suppressed to avoid a duplicate notification)
func shouldSkipSystemEvent(se services.SystemEvent) bool {
if _, isUpdate := se.Metadata["new_version_available"]; isUpdate {
return true
}
if _, isProgress := se.Metadata["progress_window"]; isProgress {
return true
}
if se.Category == "network" && se.Metadata["network"] == "::/0" {
return true
}
return false
}