windows improvements

This commit is contained in:
popaprozac 2025-04-22 14:23:32 -07:00
commit 9fcede47f7
5 changed files with 63 additions and 107 deletions

View file

@ -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.

View file

@ -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
}

View file

@ -5,8 +5,9 @@
#import <Foundation/Foundation.h>
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);

View file

@ -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, ^{

View file

@ -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,
}