mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-26 08:39:06 +02:00
[client] Use WinRT COM for Windows toasts (#6013)
* Use WinRT COM for Windows toasts instead of fyne's PowerShell path * Quote autostart path and split HKCU registry into per-user component
This commit is contained in:
@@ -42,6 +42,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||
"github.com/netbirdio/netbird/client/ui/event"
|
||||
"github.com/netbirdio/netbird/client/ui/notifier"
|
||||
"github.com/netbirdio/netbird/client/ui/process"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
|
||||
@@ -260,6 +261,7 @@ type serviceClient struct {
|
||||
|
||||
// application with main windows.
|
||||
app fyne.App
|
||||
notifier notifier.Notifier
|
||||
wSettings fyne.Window
|
||||
showAdvancedSettings bool
|
||||
sendNotification bool
|
||||
@@ -364,6 +366,7 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
||||
cancel: cancel,
|
||||
addr: args.addr,
|
||||
app: args.app,
|
||||
notifier: notifier.New(args.app),
|
||||
logFile: args.logFile,
|
||||
sendNotification: false,
|
||||
|
||||
@@ -892,7 +895,7 @@ func (s *serviceClient) updateStatus() error {
|
||||
if err != nil {
|
||||
log.Errorf("get service status: %v", err)
|
||||
if s.connected {
|
||||
s.app.SendNotification(fyne.NewNotification("Error", "Connection to service lost"))
|
||||
s.notifier.Send("Error", "Connection to service lost")
|
||||
}
|
||||
s.setDisconnectedStatus()
|
||||
return err
|
||||
@@ -1109,7 +1112,7 @@ func (s *serviceClient) onTrayReady() {
|
||||
}
|
||||
}()
|
||||
|
||||
s.eventManager = event.NewManager(s.app, s.addr)
|
||||
s.eventManager = event.NewManager(s.notifier, s.addr)
|
||||
s.eventManager.SetNotificationsEnabled(s.mNotifications.Checked())
|
||||
s.eventManager.AddHandler(func(event *proto.SystemEvent) {
|
||||
if event.Category == proto.SystemEvent_SYSTEM {
|
||||
@@ -1548,7 +1551,7 @@ func (s *serviceClient) onUpdateAvailable(newVersion string, enforced bool) {
|
||||
|
||||
if enforced && s.lastNotifiedVersion != newVersion {
|
||||
s.lastNotifiedVersion = newVersion
|
||||
s.app.SendNotification(fyne.NewNotification("Update available", "A new version "+newVersion+" is ready to install"))
|
||||
s.notifier.Send("Update available", "A new version "+newVersion+" is ready to install")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"github.com/cenkalti/backoff/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
@@ -18,11 +17,17 @@ import (
|
||||
"github.com/netbirdio/netbird/client/ui/desktop"
|
||||
)
|
||||
|
||||
// Notifier sends desktop notifications. Defined here so the event package
|
||||
// does not depend on fyne or the platform-specific notifier implementation.
|
||||
type Notifier interface {
|
||||
Send(title, body string)
|
||||
}
|
||||
|
||||
type Handler func(*proto.SystemEvent)
|
||||
|
||||
type Manager struct {
|
||||
app fyne.App
|
||||
addr string
|
||||
notifier Notifier
|
||||
addr string
|
||||
|
||||
mu sync.Mutex
|
||||
ctx context.Context
|
||||
@@ -31,10 +36,10 @@ type Manager struct {
|
||||
handlers []Handler
|
||||
}
|
||||
|
||||
func NewManager(app fyne.App, addr string) *Manager {
|
||||
func NewManager(notifier Notifier, addr string) *Manager {
|
||||
return &Manager{
|
||||
app: app,
|
||||
addr: addr,
|
||||
notifier: notifier,
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +119,7 @@ func (e *Manager) handleEvent(event *proto.SystemEvent) {
|
||||
if id != "" {
|
||||
body += fmt.Sprintf(" ID: %s", id)
|
||||
}
|
||||
e.app.SendNotification(fyne.NewNotification(title, body))
|
||||
e.notifier.Send(title, body)
|
||||
}
|
||||
|
||||
for _, handler := range handlers {
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/systray"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -87,7 +86,7 @@ func (h *eventHandler) handleConnectClick() {
|
||||
if errors.Is(err, context.Canceled) || (ok && st.Code() == codes.Canceled) {
|
||||
log.Debugf("connect operation cancelled by user")
|
||||
} else {
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to connect"))
|
||||
h.client.notifier.Send("Error", "Failed to connect")
|
||||
log.Errorf("connect failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -112,7 +111,7 @@ func (h *eventHandler) handleDisconnectClick() {
|
||||
if err := h.client.menuDownClick(); err != nil {
|
||||
st, ok := status.FromError(err)
|
||||
if !errors.Is(err, context.Canceled) && !(ok && st.Code() == codes.Canceled) {
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to disconnect"))
|
||||
h.client.notifier.Send("Error", "Failed to disconnect")
|
||||
log.Errorf("disconnect failed: %v", err)
|
||||
} else {
|
||||
log.Debugf("disconnect cancelled or already disconnecting")
|
||||
@@ -130,7 +129,7 @@ func (h *eventHandler) handleAllowSSHClick() {
|
||||
if err := h.updateConfigWithErr(); err != nil {
|
||||
h.toggleCheckbox(h.client.mAllowSSH) // revert checkbox state on error
|
||||
log.Errorf("failed to update config: %v", err)
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update SSH settings"))
|
||||
h.client.notifier.Send("Error", "Failed to update SSH settings")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -140,7 +139,7 @@ func (h *eventHandler) handleAutoConnectClick() {
|
||||
if err := h.updateConfigWithErr(); err != nil {
|
||||
h.toggleCheckbox(h.client.mAutoConnect) // revert checkbox state on error
|
||||
log.Errorf("failed to update config: %v", err)
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update auto-connect settings"))
|
||||
h.client.notifier.Send("Error", "Failed to update auto-connect settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +148,7 @@ func (h *eventHandler) handleRosenpassClick() {
|
||||
if err := h.updateConfigWithErr(); err != nil {
|
||||
h.toggleCheckbox(h.client.mEnableRosenpass) // revert checkbox state on error
|
||||
log.Errorf("failed to update config: %v", err)
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update Rosenpass settings"))
|
||||
h.client.notifier.Send("Error", "Failed to update Rosenpass settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +157,7 @@ func (h *eventHandler) handleLazyConnectionClick() {
|
||||
if err := h.updateConfigWithErr(); err != nil {
|
||||
h.toggleCheckbox(h.client.mLazyConnEnabled) // revert checkbox state on error
|
||||
log.Errorf("failed to update config: %v", err)
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update lazy connection settings"))
|
||||
h.client.notifier.Send("Error", "Failed to update lazy connection settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,7 +166,7 @@ func (h *eventHandler) handleBlockInboundClick() {
|
||||
if err := h.updateConfigWithErr(); err != nil {
|
||||
h.toggleCheckbox(h.client.mBlockInbound) // revert checkbox state on error
|
||||
log.Errorf("failed to update config: %v", err)
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update block inbound settings"))
|
||||
h.client.notifier.Send("Error", "Failed to update block inbound settings")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +175,7 @@ func (h *eventHandler) handleNotificationsClick() {
|
||||
if err := h.updateConfigWithErr(); err != nil {
|
||||
h.toggleCheckbox(h.client.mNotifications) // revert checkbox state on error
|
||||
log.Errorf("failed to update config: %v", err)
|
||||
h.client.app.SendNotification(fyne.NewNotification("Error", "Failed to update notifications settings"))
|
||||
h.client.notifier.Send("Error", "Failed to update notifications settings")
|
||||
} else if h.client.eventManager != nil {
|
||||
h.client.eventManager.SetNotificationsEnabled(h.client.mNotifications.Checked())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Package notifier sends desktop notifications. On Windows it uses the WinRT
|
||||
// COM API directly via go-toast/v2 to avoid the PowerShell window flash that
|
||||
// fyne's default implementation produces. On other platforms it delegates to
|
||||
// fyne.
|
||||
package notifier
|
||||
|
||||
import "fyne.io/fyne/v2"
|
||||
|
||||
// Notifier sends desktop notifications.
|
||||
type Notifier interface {
|
||||
Send(title, body string)
|
||||
}
|
||||
|
||||
// New returns a platform-specific Notifier. The fyne app is used as the
|
||||
// fallback notifier on platforms where no native implementation is wired up,
|
||||
// and on Windows when the COM path fails to initialize.
|
||||
func New(app fyne.App) Notifier {
|
||||
return newNotifier(app)
|
||||
}
|
||||
|
||||
type fyneNotifier struct {
|
||||
app fyne.App
|
||||
}
|
||||
|
||||
func (f *fyneNotifier) Send(title, body string) {
|
||||
f.app.SendNotification(fyne.NewNotification(title, body))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package notifier
|
||||
|
||||
import "fyne.io/fyne/v2"
|
||||
|
||||
func newNotifier(app fyne.App) Notifier {
|
||||
return &fyneNotifier{app: app}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
toast "git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||
"git.sr.ht/~jackmordaunt/go-toast/v2/wintoast"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// appID is the AppUserModelID shown in the Windows Action Center. It
|
||||
// must match the System.AppUserModel.ID property set on the Start Menu
|
||||
// shortcut by the MSI (see client/netbird.wxs); otherwise Windows
|
||||
// groups toasts under a separate, unbranded entry.
|
||||
appID = "NetBird"
|
||||
|
||||
// appGUID identifies the COM activation callback class. Generated once
|
||||
// for NetBird; do not change without coordinating an installer bump,
|
||||
// since old registry entries pointing at the previous GUID would orphan.
|
||||
appGUID = "{0E1B4DE7-E148-432B-9814-544F941826EC}"
|
||||
)
|
||||
|
||||
type comNotifier struct {
|
||||
fallback *fyneNotifier
|
||||
ready bool
|
||||
iconPath string
|
||||
}
|
||||
|
||||
var (
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
)
|
||||
|
||||
func newNotifier(app fyne.App) Notifier {
|
||||
n := &comNotifier{
|
||||
fallback: &fyneNotifier{app: app},
|
||||
iconPath: resolveIcon(),
|
||||
}
|
||||
initOnce.Do(func() {
|
||||
initErr = wintoast.SetAppData(wintoast.AppData{
|
||||
AppID: appID,
|
||||
GUID: appGUID,
|
||||
IconPath: n.iconPath,
|
||||
})
|
||||
})
|
||||
if initErr != nil {
|
||||
log.Warnf("toast: register app data failed, falling back to fyne notifications: %v", initErr)
|
||||
return n.fallback
|
||||
}
|
||||
n.ready = true
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *comNotifier) Send(title, body string) {
|
||||
if !n.ready {
|
||||
n.fallback.Send(title, body)
|
||||
return
|
||||
}
|
||||
notification := toast.Notification{
|
||||
AppID: appID,
|
||||
Title: title,
|
||||
Body: body,
|
||||
Icon: n.iconPath,
|
||||
}
|
||||
if err := notification.Push(); err != nil {
|
||||
log.Warnf("toast: push failed, using fyne fallback: %v", err)
|
||||
n.fallback.Send(title, body)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveIcon returns an absolute path to the toast icon, or an empty string
|
||||
// when no icon can be located. Windows requires a PNG/JPG for the
|
||||
// AppUserModelId IconUri registry value; .ico is silently ignored.
|
||||
func resolveIcon() string {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
candidate := filepath.Join(filepath.Dir(exe), "netbird.png")
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
return candidate
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -548,7 +548,7 @@ func (p *profileMenu) refresh() {
|
||||
if err != nil {
|
||||
log.Errorf("failed to switch profile: %v", err)
|
||||
// show notification dialog
|
||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to switch profile"))
|
||||
p.serviceClient.notifier.Send("Error", "Failed to switch profile")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -628,9 +628,9 @@ func (p *profileMenu) refresh() {
|
||||
}
|
||||
if err := p.eventHandler.logout(p.ctx); err != nil {
|
||||
log.Errorf("logout failed: %v", err)
|
||||
p.app.SendNotification(fyne.NewNotification("Error", "Failed to deregister"))
|
||||
p.serviceClient.notifier.Send("Error", "Failed to deregister")
|
||||
} else {
|
||||
p.app.SendNotification(fyne.NewNotification("Success", "Deregistered successfully"))
|
||||
p.serviceClient.notifier.Send("Success", "Deregistered successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user