5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-06 21:40:12 +08:00

more cleanup

This commit is contained in:
popaprozac 2025-03-19 18:10:59 -07:00
parent ee885fea44
commit 4af058bd02
5 changed files with 103 additions and 59 deletions

View File

@ -1,10 +1,25 @@
// Package notifications provides cross-platform notification capabilities for desktop applications.
// It supports macOS, Windows, and Linux with a consistent API while handling platform-specific
// differences internally. Key features include:
// - Basic notifications with title, subtitle, and body
// - Interactive notifications with buttons and actions
// - Notification categories for reusing configurations
// - User feedback handling with a unified callback system
//
// Platform-specific notes:
// - macOS: Requires a properly bundled and signed application
// - Windows: Uses Windows Toast notifications
// - Linux: Falls back between D-Bus, notify-send, or other methods and does not support text inputs
package notifications package notifications
import "sync" import (
"fmt"
"sync"
)
// Service represents the notifications service // Service represents the notifications service
type Service struct { type Service struct {
// notificationResponseCallback is called when a notification response is received // notificationResponseCallback is called when a notification result is received.
// Only one callback can be assigned at a time. // Only one callback can be assigned at a time.
notificationResultCallback func(result NotificationResult) notificationResultCallback func(result NotificationResult)
@ -93,3 +108,15 @@ func (ns *Service) handleNotificationResult(result NotificationResult) {
callback(result) callback(result)
} }
} }
func validateNotificationOptions(options NotificationOptions) error {
if options.ID == "" {
return fmt.Errorf("notification ID cannot be empty")
}
if options.Title == "" {
return fmt.Errorf("notification title cannot be empty")
}
return nil
}

View File

@ -56,7 +56,7 @@ func CheckBundleIdentifier() bool {
} }
// RequestNotificationAuthorization requests permission for notifications. // RequestNotificationAuthorization requests permission for notifications.
// Default timeout is 5 minutes // Default timeout is 15 minutes
func (ns *Service) RequestNotificationAuthorization() (bool, error) { func (ns *Service) RequestNotificationAuthorization() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*900) ctx, cancel := context.WithTimeout(context.Background(), time.Second*900)
defer cancel() defer cancel()
@ -94,6 +94,10 @@ func (ns *Service) CheckNotificationAuthorization() (bool, error) {
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body. // SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
func (ns *Service) SendNotification(options NotificationOptions) error { func (ns *Service) SendNotification(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -139,6 +143,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category. // A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
// If a NotificationCategory is not registered a basic notification will be sent. // If a NotificationCategory is not registered a basic notification will be sent.
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error { func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
@ -243,7 +251,7 @@ func (ns *Service) RemoveNotificationCategory(categoryId string) error {
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
} }
return fmt.Errorf("category registration failed") return fmt.Errorf("category removal failed")
} }
return nil return nil
case <-ctx.Done(): case <-ctx.Done():

View File

@ -106,37 +106,43 @@ void checkNotificationAuthorization(int channelID) {
}]; }];
} }
void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) { // Helper function to create notification content
ensureDelegateInitialized(); UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle,
const char *body, const char *data_json) {
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
NSString *nsTitle = [NSString stringWithUTF8String:title]; NSString *nsTitle = [NSString stringWithUTF8String:title];
NSString *nsSubtitle = [NSString stringWithUTF8String:subtitle]; NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
NSString *nsBody = [NSString stringWithUTF8String:body]; NSString *nsBody = [NSString stringWithUTF8String:body];
NSMutableDictionary *customData = [NSMutableDictionary dictionary]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = nsTitle;
if (![nsSubtitle isEqualToString:@""]) {
content.subtitle = nsSubtitle;
}
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
// Parse JSON data if provided
if (data_json) { if (data_json) {
NSString *dataJsonStr = [NSString stringWithUTF8String:data_json]; NSString *dataJsonStr = [NSString stringWithUTF8String:data_json];
NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding]; NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil; NSError *error = nil;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error) { if (!error && parsedData) {
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; content.userInfo = parsedData;
captureResult(channelID, false, [errorMsg UTF8String]);
return;
}
if (parsedData) {
[customData addEntriesFromDictionary:parsedData];
} }
} }
return content;
}
void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
ensureDelegateInitialized();
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
content.title = nsTitle; UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
content.subtitle = nsSubtitle; NSMutableDictionary *customData = [NSMutableDictionary dictionary];
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
if (customData.count > 0) { if (customData.count > 0) {
content.userInfo = customData; content.userInfo = customData;
@ -157,35 +163,16 @@ void sendNotification(int channelID, const char *identifier, const char *title,
} }
void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle,
const char *body, const char *categoryId, const char *actions_json) { const char *body, const char *categoryId, const char *data_json) {
ensureDelegateInitialized(); ensureDelegateInitialized();
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
NSString *nsTitle = [NSString stringWithUTF8String:title];
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
NSString *nsBody = [NSString stringWithUTF8String:body];
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
if (actions_json) {
NSString *actionsJsonStr = [NSString stringWithUTF8String:actions_json];
NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (!error && parsedData) {
[customData addEntriesFromDictionary:parsedData];
}
}
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
content.title = nsTitle; NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
if (![nsSubtitle isEqualToString:@""]) { UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
content.subtitle = nsSubtitle; NSMutableDictionary *customData = [NSMutableDictionary dictionary];
}
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
content.categoryIdentifier = nsCategoryId; content.categoryIdentifier = nsCategoryId;
if (customData.count > 0) { if (customData.count > 0) {

View File

@ -415,6 +415,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
return errors.New("notification service not initialized") return errors.New("notification service not initialized")
} }
if err := validateNotificationOptions(options); err != nil {
return err
}
notifier.Lock() notifier.Lock()
defer notifier.Unlock() defer notifier.Unlock()
@ -454,6 +458,10 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
return errors.New("notification service not initialized") return errors.New("notification service not initialized")
} }
if err := validateNotificationOptions(options); err != nil {
return err
}
notificationLock.RLock() notificationLock.RLock()
category, exists := notificationCategories[options.CategoryID] category, exists := notificationCategories[options.CategoryID]
notificationLock.RUnlock() notificationLock.RUnlock()

View File

@ -128,6 +128,10 @@ func (ns *Service) CheckNotificationAuthorization() bool {
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows. // SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
// (subtitle and category id are only available on macOS) // (subtitle and category id are only available on macOS)
func (ns *Service) SendNotification(options NotificationOptions) error { func (ns *Service) SendNotification(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
if err := saveIconToDir(); err != nil { if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err) fmt.Printf("Error saving icon: %v\n", err)
} }
@ -140,9 +144,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
if options.Data != nil { if options.Data != nil {
encodedPayload, err := encodePayload(DefaultActionIdentifier, options.Data) encodedPayload, err := encodePayload(DefaultActionIdentifier, options.Data)
if err == nil { if err != nil {
n.ActivationArguments = encodedPayload return fmt.Errorf("failed to encode notification data: %w", err)
} }
n.ActivationArguments = encodedPayload
} }
return n.Push() return n.Push()
@ -153,6 +158,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
// If a NotificationCategory is not registered a basic notification will be sent. // If a NotificationCategory is not registered a basic notification will be sent.
// (subtitle and category id are only available on macOS) // (subtitle and category id are only available on macOS)
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error { func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
if err := saveIconToDir(); err != nil { if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err) fmt.Printf("Error saving icon: %v\n", err)
} }
@ -191,15 +200,15 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
n.ActivationArguments, _ = encodePayload(n.ActivationArguments, options.Data) n.ActivationArguments, _ = encodePayload(n.ActivationArguments, options.Data)
for index := range n.Actions { for index := range n.Actions {
n.Actions[index].Arguments, _ = encodePayload(n.Actions[index].Arguments, options.Data) encodedPayload, err := encodePayload(n.Actions[index].Arguments, options.Data)
if err != nil {
return fmt.Errorf("failed to encode notification data: %w", err)
}
n.Actions[index].Arguments = encodedPayload
} }
} }
err := n.Push() return n.Push()
if err != nil {
return err
}
return nil
} }
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions. // RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
@ -344,20 +353,25 @@ func loadCategoriesFromRegistry() error {
) )
if err != nil { if err != nil {
if err == registry.ErrNotExist { if err == registry.ErrNotExist {
// Not an error, no saved categories
return nil return nil
} }
return err return fmt.Errorf("failed to open registry key: %w", err)
} }
defer key.Close() defer key.Close()
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey) data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
if err != nil { if err != nil {
return err if err == registry.ErrNotExist {
// No value yet, but key exists
return nil
}
return fmt.Errorf("failed to read categories from registry: %w", err)
} }
categories := make(map[string]NotificationCategory) categories := make(map[string]NotificationCategory)
if err := json.Unmarshal([]byte(data), &categories); err != nil { if err := json.Unmarshal([]byte(data), &categories); err != nil {
return err return fmt.Errorf("failed to parse notification categories from registry: %w", err)
} }
notificationCategoriesLock.Lock() notificationCategoriesLock.Lock()