5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-06 19:11:14 +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
import "sync"
import (
"fmt"
"sync"
)
// Service represents the notifications service
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.
notificationResultCallback func(result NotificationResult)
@ -93,3 +108,15 @@ func (ns *Service) handleNotificationResult(result NotificationResult) {
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.
// Default timeout is 5 minutes
// Default timeout is 15 minutes
func (ns *Service) RequestNotificationAuthorization() (bool, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*900)
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.
func (ns *Service) SendNotification(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
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.
// If a NotificationCategory is not registered a basic notification will be sent.
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@ -243,7 +251,7 @@ func (ns *Service) RemoveNotificationCategory(categoryId string) error {
if result.Error != nil {
return result.Error
}
return fmt.Errorf("category registration failed")
return fmt.Errorf("category removal failed")
}
return nil
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) {
ensureDelegateInitialized();
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
// Helper function to create notification content
UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle,
const char *body, const char *data_json) {
NSString *nsTitle = [NSString stringWithUTF8String:title];
NSString *nsSubtitle = [NSString stringWithUTF8String:subtitle];
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
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) {
NSString *dataJsonStr = [NSString stringWithUTF8String:data_json];
NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
NSError *error = nil;
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
if (error) {
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
captureResult(channelID, false, [errorMsg UTF8String]);
return;
}
if (parsedData) {
[customData addEntriesFromDictionary:parsedData];
if (!error && parsedData) {
content.userInfo = 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];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = nsTitle;
content.subtitle = nsSubtitle;
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
if (customData.count > 0) {
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,
const char *body, const char *categoryId, const char *actions_json) {
const char *body, const char *categoryId, const char *data_json) {
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];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = nsTitle;
if (![nsSubtitle isEqualToString:@""]) {
content.subtitle = nsSubtitle;
}
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
content.categoryIdentifier = nsCategoryId;
if (customData.count > 0) {

View File

@ -415,6 +415,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
return errors.New("notification service not initialized")
}
if err := validateNotificationOptions(options); err != nil {
return err
}
notifier.Lock()
defer notifier.Unlock()
@ -454,6 +458,10 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
return errors.New("notification service not initialized")
}
if err := validateNotificationOptions(options); err != nil {
return err
}
notificationLock.RLock()
category, exists := notificationCategories[options.CategoryID]
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.
// (subtitle and category id are only available on macOS)
func (ns *Service) SendNotification(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err)
}
@ -140,9 +144,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
if options.Data != nil {
encodedPayload, err := encodePayload(DefaultActionIdentifier, options.Data)
if err == nil {
n.ActivationArguments = encodedPayload
if err != nil {
return fmt.Errorf("failed to encode notification data: %w", err)
}
n.ActivationArguments = encodedPayload
}
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.
// (subtitle and category id are only available on macOS)
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
if err := validateNotificationOptions(options); err != nil {
return err
}
if err := saveIconToDir(); err != nil {
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)
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()
if err != nil {
return err
}
return nil
return n.Push()
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
@ -344,20 +353,25 @@ func loadCategoriesFromRegistry() error {
)
if err != nil {
if err == registry.ErrNotExist {
// Not an error, no saved categories
return nil
}
return err
return fmt.Errorf("failed to open registry key: %w", err)
}
defer key.Close()
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
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)
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()