mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Merge pull request #4230 from popaprozac/windows-improvements
[v3] Windows notifications improvements
This commit is contained in:
commit
685d8101aa
5 changed files with 69 additions and 107 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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, ^{
|
||||
|
|
|
|||
|
|
@ -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,29 @@ 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, _, err := registry.CreateKey(registry.CURRENT_USER,
|
||||
`Software\Classes\CLSID\`+wn.appGUID+`\LocalServer32`, registry.ALL_ACCESS)
|
||||
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{
|
||||
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 +111,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 +173,7 @@ func (wn *windowsNotifier) SendNotification(options NotificationOptions) error {
|
|||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
ActivationType: toast.Foreground,
|
||||
ActivationArguments: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +208,7 @@ func (wn *windowsNotifier) SendNotificationWithActions(options NotificationOptio
|
|||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
ActivationType: toast.Foreground,
|
||||
ActivationArguments: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue