From 9fcede47f7477c0ed19d0fd05b0a55153b04cb79 Mon Sep 17 00:00:00 2001 From: popaprozac Date: Tue, 22 Apr 2025 14:23:32 -0700 Subject: [PATCH 1/2] windows improvements --- v3/examples/notifications/main.go | 92 ++----------------- .../notifications/notifications_darwin.go | 3 + .../notifications/notifications_darwin.h | 3 +- .../notifications/notifications_darwin.m | 2 +- .../notifications/notifications_windows.go | 74 ++++++++++----- 5 files changed, 65 insertions(+), 109 deletions(-) diff --git a/v3/examples/notifications/main.go b/v3/examples/notifications/main.go index 264e7273e..75f0551a5 100644 --- a/v3/examples/notifications/main.go +++ b/v3/examples/notifications/main.go @@ -5,10 +5,8 @@ import ( _ "embed" "fmt" "log" - "time" "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/events" "github.com/wailsapp/wails/v3/pkg/services/notifications" ) @@ -53,6 +51,7 @@ func main() { // 'URL' is the URL that will be loaded into the webview. app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ Title: "Window 1", + Name: "main", Mac: application.MacWindow{ InvisibleTitleBarHeight: 50, Backdrop: application.MacBackdropTranslucent, @@ -62,86 +61,15 @@ func main() { URL: "/", }) - app.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { - // Create a goroutine that spawns desktop notifications from Go - go func() { - var authorized bool - var err error - authorized, err = ns.CheckNotificationAuthorization() - if err != nil { - println(fmt.Errorf("checking app notification authorization failed: %s", err)) - } - - if !authorized { - authorized, err = ns.RequestNotificationAuthorization() - if err != nil { - println(fmt.Errorf("requesting app notification authorization failed: %s", err)) - } - } - - if authorized { - ns.OnNotificationResponse(func(result notifications.NotificationResult) { - if result.Error != nil { - println(fmt.Errorf("parsing notification result failed: %s", result.Error)) - } else { - fmt.Printf("Response: %+v\n", result.Response) - println("Sending response to frontend...") - app.EmitEvent("notification:action", result.Response) - } - }) - - err = ns.SendNotification(notifications.NotificationOptions{ - ID: "uuid-basic-1", - Title: "Notification Title", - Subtitle: "Subtitle on macOS and Linux", - Body: "Body text of notification.", - Data: map[string]interface{}{ - "user-id": "user-123", - "message-id": "msg-123", - "timestamp": time.Now().Unix(), - }, - }) - if err != nil { - println(fmt.Errorf("sending basic notification failed: %s", err)) - } - - // Delay before sending next notification - time.Sleep(time.Second * 2) - - const CategoryID = "backend-notification-id" - - err = ns.RegisterNotificationCategory(notifications.NotificationCategory{ - ID: CategoryID, - Actions: []notifications.NotificationAction{ - {ID: "VIEW", Title: "View"}, - {ID: "MARK_READ", Title: "Mark as read"}, - {ID: "DELETE", Title: "Delete", Destructive: true}, - }, - HasReplyField: true, - ReplyPlaceholder: "Message...", - ReplyButtonTitle: "Reply", - }) - if err != nil { - println(fmt.Errorf("creating notification category failed: %s", err)) - } - - err = ns.SendNotificationWithActions(notifications.NotificationOptions{ - ID: "uuid-with-actions-1", - Title: "Actions Notification Title", - Subtitle: "Subtitle on macOS and Linux", - Body: "Body text of notification with actions.", - CategoryID: CategoryID, - Data: map[string]interface{}{ - "user-id": "user-123", - "message-id": "msg-123", - "timestamp": time.Now().Unix(), - }, - }) - if err != nil { - println(fmt.Errorf("sending notification with actions failed: %s", err)) - } - } - }() + // Pass a notification callback that will be called when a notification is actioned. + ns.OnNotificationResponse(func(result notifications.NotificationResult) { + if result.Error != nil { + println(fmt.Errorf("parsing notification result failed: %s", result.Error)) + } else { + fmt.Printf("Response: %+v\n", result.Response) + println("Sending response to frontend...") + app.EmitEvent("notification:action", result.Response) + } }) // Run the application. This blocks until the application has been exited. diff --git a/v3/pkg/services/notifications/notifications_darwin.go b/v3/pkg/services/notifications/notifications_darwin.go index 2c8f33d15..75ac642fe 100644 --- a/v3/pkg/services/notifications/notifications_darwin.go +++ b/v3/pkg/services/notifications/notifications_darwin.go @@ -65,6 +65,9 @@ func (dn *darwinNotifier) Startup(ctx context.Context, options application.Servi if !checkBundleIdentifier() { return fmt.Errorf("notifications require a valid bundle identifier") } + if !bool(C.ensureDelegateInitialized()) { + return fmt.Errorf("failed to initialize notification center delegate") + } return nil } diff --git a/v3/pkg/services/notifications/notifications_darwin.h b/v3/pkg/services/notifications/notifications_darwin.h index 7cd505240..71e167656 100644 --- a/v3/pkg/services/notifications/notifications_darwin.h +++ b/v3/pkg/services/notifications/notifications_darwin.h @@ -5,8 +5,9 @@ #import -bool checkBundleIdentifier(void); bool isNotificationAvailable(void); +bool checkBundleIdentifier(void); +bool ensureDelegateInitialized(void); void requestNotificationAuthorization(int channelID); void checkNotificationAuthorization(int channelID); void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json); diff --git a/v3/pkg/services/notifications/notifications_darwin.m b/v3/pkg/services/notifications/notifications_darwin.m index 6c2048b74..373197cb0 100644 --- a/v3/pkg/services/notifications/notifications_darwin.m +++ b/v3/pkg/services/notifications/notifications_darwin.m @@ -91,7 +91,7 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response static NotificationsDelegate *delegateInstance = nil; static dispatch_once_t onceToken; -static BOOL ensureDelegateInitialized(void) { +bool ensureDelegateInitialized(void) { __block BOOL success = YES; dispatch_once(&onceToken, ^{ diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go index b7a16decc..18fb1ddf0 100644 --- a/v3/pkg/services/notifications/notifications_windows.go +++ b/v3/pkg/services/notifications/notifications_windows.go @@ -4,15 +4,16 @@ package notifications import ( "context" - _ "embed" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "sync" + _ "unsafe" "git.sr.ht/~jackmordaunt/go-toast/v2" + wintoast "git.sr.ht/~jackmordaunt/go-toast/v2/wintoast" "github.com/google/uuid" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/w32" @@ -25,6 +26,7 @@ type windowsNotifier struct { appName string appGUID string iconPath string + exePath string } const ( @@ -55,13 +57,19 @@ func New() *Service { return NotificationService } +//go:linkname registerFactoryInternal git.sr.ht/~jackmordaunt/go-toast/v2/wintoast.registerClassFactory +func registerFactoryInternal(factory *wintoast.IClassFactory) error + // Startup is called when the service is loaded // Sets an activation callback to emit an event when notifications are interacted with. func (wn *windowsNotifier) Startup(ctx context.Context, options application.ServiceOptions) error { wn.categoriesLock.Lock() defer wn.categoriesLock.Unlock() - wn.appName = application.Get().Config().Name + app := application.Get() + cfg := app.Config() + + wn.appName = cfg.Name guid, err := wn.getGUID() if err != nil { @@ -71,10 +79,23 @@ func (wn *windowsNotifier) Startup(ctx context.Context, options application.Serv wn.iconPath = filepath.Join(os.TempDir(), wn.appName+wn.appGUID+".png") + exe, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %w", err) + } + wn.exePath = exe + + // Create the registry key for the toast activator + key, _, _ := registry.CreateKey(registry.CURRENT_USER, + `Software\Classes\CLSID\`+wn.appGUID+`\LocalServer32`, registry.ALL_ACCESS) + key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", wn.exePath)) + key.Close() + toast.SetAppData(toast.AppData{ - AppID: wn.appName, - GUID: guid, - IconPath: wn.iconPath, + AppID: wn.appName, + GUID: guid, + IconPath: wn.iconPath, + ActivationExe: wn.exePath, }) toast.SetActivationCallback(func(args string, data []toast.UserData) { @@ -84,34 +105,35 @@ func (wn *windowsNotifier) Startup(ctx context.Context, options application.Serv if err != nil { result.Error = err - - if ns := getNotificationService(); ns != nil { - ns.handleNotificationResult(result) + } else { + // Subtitle is retained but was not shown with the notification + response := NotificationResponse{ + ID: options.ID, + ActionIdentifier: actionIdentifier, + Title: options.Title, + Subtitle: options.Subtitle, + Body: options.Body, + CategoryID: options.CategoryID, + UserInfo: options.Data, } - return + + if userText, found := wn.getUserText(data); found { + response.UserText = userText + } + + result.Response = response } - // Subtitle is retained but was not shown with the notification - response := NotificationResponse{ - ID: options.ID, - ActionIdentifier: actionIdentifier, - Title: options.Title, - Subtitle: options.Subtitle, - Body: options.Body, - CategoryID: options.CategoryID, - UserInfo: options.Data, - } - - if userText, found := wn.getUserText(data); found { - response.UserText = userText - } - - result.Response = response if ns := getNotificationService(); ns != nil { ns.handleNotificationResult(result) } }) + // Register the class factory for the toast activator + if err := registerFactoryInternal(wintoast.ClassFactory); err != nil { + return fmt.Errorf("CoRegisterClassObject failed: %w", err) + } + return wn.loadCategoriesFromRegistry() } @@ -145,6 +167,7 @@ func (wn *windowsNotifier) SendNotification(options NotificationOptions) error { n := toast.Notification{ Title: options.Title, Body: options.Body, + ActivationType: toast.Foreground, ActivationArguments: DefaultActionIdentifier, } @@ -179,6 +202,7 @@ func (wn *windowsNotifier) SendNotificationWithActions(options NotificationOptio n := toast.Notification{ Title: options.Title, Body: options.Body, + ActivationType: toast.Foreground, ActivationArguments: DefaultActionIdentifier, } From 66ba876a652222116bb9193713448a6b2292ecac Mon Sep 17 00:00:00 2001 From: popaprozac Date: Tue, 22 Apr 2025 16:21:33 -0700 Subject: [PATCH 2/2] better errors --- v3/pkg/services/notifications/notifications_windows.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go index 18fb1ddf0..bade71f85 100644 --- a/v3/pkg/services/notifications/notifications_windows.go +++ b/v3/pkg/services/notifications/notifications_windows.go @@ -86,9 +86,15 @@ func (wn *windowsNotifier) Startup(ctx context.Context, options application.Serv wn.exePath = exe // Create the registry key for the toast activator - key, _, _ := registry.CreateKey(registry.CURRENT_USER, + key, _, err := registry.CreateKey(registry.CURRENT_USER, `Software\Classes\CLSID\`+wn.appGUID+`\LocalServer32`, registry.ALL_ACCESS) - key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", wn.exePath)) + if err != nil { + return fmt.Errorf("failed to create CLSID key: %w", err) + } + + if err := key.SetStringValue("", fmt.Sprintf("\"%s\" %%1", wn.exePath)); err != nil { + return fmt.Errorf("failed to set CLSID server path: %w", err) + } key.Close() toast.SetAppData(toast.AppData{