diff --git a/v3/examples/notifications/frontend/main.js b/v3/examples/notifications/frontend/main.js index 2c6d234d4..781f8983e 100644 --- a/v3/examples/notifications/frontend/main.js +++ b/v3/examples/notifications/frontend/main.js @@ -48,6 +48,20 @@ window.sendComplexNotification = async () => { } } +window.removeNotification = async (id) => { + const granted = await Notifications.Service.RequestNotificationAuthorization(); + if (granted) { + await Notifications.Service.RemoveDeliveredNotification(id); + } +} + +window.removeAllNotifications = async (id) => { + const granted = await Notifications.Service.RequestNotificationAuthorization(); + if (granted) { + await Notifications.Service.RemoveAllDeliveredNotifications(); + } +} + Events.On("notification:response", (response) => { notificationsElement.innerText = JSON.stringify(response.data[0]); }); \ No newline at end of file diff --git a/v3/pkg/services/notifications/notifications.go b/v3/pkg/services/notifications/notifications.go index ccbcc7e1c..fb519aed2 100644 --- a/v3/pkg/services/notifications/notifications.go +++ b/v3/pkg/services/notifications/notifications.go @@ -5,6 +5,7 @@ import "sync" // Service represents the notifications service type Service struct { // notificationResponseCallback is called when a notification response is received + // Only one callback can be assigned at a time. notificationResultCallback func(result NotificationResult) callbackLock sync.RWMutex diff --git a/v3/pkg/services/notifications/notifications_darwin.go b/v3/pkg/services/notifications/notifications_darwin.go index 88769fe04..217e75ad4 100644 --- a/v3/pkg/services/notifications/notifications_darwin.go +++ b/v3/pkg/services/notifications/notifications_darwin.go @@ -18,8 +18,8 @@ import ( ) type notificationChannel struct { - success bool - err error + Success bool + Error error } var ( @@ -66,7 +66,7 @@ func (ns *Service) RequestNotificationAuthorization() (bool, error) { select { case result := <-resultCh: - return result.success, result.err + return result.Success, result.Error case <-ctx.Done(): cleanupChannel(id) return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err()) @@ -84,7 +84,7 @@ func (ns *Service) CheckNotificationAuthorization() (bool, error) { select { case result := <-resultCh: - return result.success, result.err + return result.Success, result.Error case <-ctx.Done(): cleanupChannel(id) return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err()) @@ -121,9 +121,9 @@ func (ns *Service) SendNotification(options NotificationOptions) error { select { case result := <-resultCh: - if !result.success { - if result.err != nil { - return result.err + if !result.Success { + if result.Error != nil { + return result.Error } return fmt.Errorf("sending notification failed") } @@ -167,9 +167,9 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro C.sendNotificationWithActions(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON) select { case result := <-resultCh: - if !result.success { - if result.err != nil { - return result.err + if !result.Success { + if result.Error != nil { + return result.Error } return fmt.Errorf("sending notification failed") } @@ -211,9 +211,9 @@ func (ns *Service) RegisterNotificationCategory(category NotificationCategory) e select { case result := <-resultCh: - if !result.success { - if result.err != nil { - return result.err + if !result.Success { + if result.Error != nil { + return result.Error } return fmt.Errorf("category registration failed") } @@ -226,11 +226,29 @@ func (ns *Service) RegisterNotificationCategory(category NotificationCategory) e // RemoveNotificationCategory remove a previously registered NotificationCategory. func (ns *Service) RemoveNotificationCategory(categoryId string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + id, resultCh := registerChannel() + cCategoryID := C.CString(categoryId) defer C.free(unsafe.Pointer(cCategoryID)) - C.removeNotificationCategory(cCategoryID) - return nil + C.removeNotificationCategory(C.int(id), cCategoryID) + + select { + case result := <-resultCh: + if !result.Success { + if result.Error != nil { + return result.Error + } + return fmt.Errorf("category registration failed") + } + return nil + case <-ctx.Done(): + cleanupChannel(id) + return fmt.Errorf("category registration timed out: %w", ctx.Err()) + } } // RemoveAllPendingNotifications removes all pending notifications. @@ -272,8 +290,8 @@ func (ns *Service) RemoveNotification(identifier string) error { return nil } -//export notificationResponse -func notificationResponse(channelID C.int, success C.bool, errorMsg *C.char) { +//export captureResult +func captureResult(channelID C.int, success C.bool, errorMsg *C.char) { resultCh, exists := getChannel(int(channelID)) if !exists { // handle this @@ -287,8 +305,8 @@ func notificationResponse(channelID C.int, success C.bool, errorMsg *C.char) { } resultCh <- notificationChannel{ - success: bool(success), - err: err, + Success: bool(success), + Error: err, } close(resultCh) diff --git a/v3/pkg/services/notifications/notifications_darwin.h b/v3/pkg/services/notifications/notifications_darwin.h index a8b7c2298..f7acf6d1a 100644 --- a/v3/pkg/services/notifications/notifications_darwin.h +++ b/v3/pkg/services/notifications/notifications_darwin.h @@ -11,7 +11,7 @@ void checkNotificationAuthorization(int channelID); void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json); void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json); void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle); -void removeNotificationCategory(const char *categoryId); +void removeNotificationCategory(int channelID, const char *categoryId); void removeAllPendingNotifications(void); void removePendingNotification(const char *identifier); void removeAllDeliveredNotifications(void); diff --git a/v3/pkg/services/notifications/notifications_darwin.m b/v3/pkg/services/notifications/notifications_darwin.m index 66461df99..abb129842 100644 --- a/v3/pkg/services/notifications/notifications_darwin.m +++ b/v3/pkg/services/notifications/notifications_darwin.m @@ -2,7 +2,7 @@ #import #import -extern void notificationResponse(int channelID, bool success, const char* error); +extern void captureResult(int channelID, bool success, const char* error); extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error); @interface NotificationsDelegate : NSObject @@ -89,9 +89,9 @@ void requestNotificationAuthorization(int channelID) { [center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) { if (error) { NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; - notificationResponse(channelID, false, [errorMsg UTF8String]); + captureResult(channelID, false, [errorMsg UTF8String]); } else { - notificationResponse(channelID, granted, NULL); + captureResult(channelID, granted, NULL); } }]; } @@ -102,7 +102,7 @@ void checkNotificationAuthorization(int channelID) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized); - notificationResponse(channelID, isAuthorized, NULL); + captureResult(channelID, isAuthorized, NULL); }]; } @@ -122,10 +122,10 @@ void sendNotification(int channelID, const char *identifier, const char *title, NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error]; if (error) { NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; - notificationResponse(channelID, false, [errorMsg UTF8String]); + captureResult(channelID, false, [errorMsg UTF8String]); return; } - if (!error && parsedData) { + if (parsedData) { [customData addEntriesFromDictionary:parsedData]; } } @@ -142,16 +142,16 @@ void sendNotification(int channelID, const char *identifier, const char *title, content.userInfo = customData; } - UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO]; + UNTimeIntervalNotificationTrigger *trigger = nil; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger]; [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (error) { NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; - notificationResponse(channelID, false, [errorMsg UTF8String]); + captureResult(channelID, false, [errorMsg UTF8String]); } else { - notificationResponse(channelID, true, NULL); + captureResult(channelID, true, NULL); } }]; } @@ -192,16 +192,16 @@ void sendNotificationWithActions(int channelID, const char *identifier, const ch content.userInfo = customData; } - UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO]; + UNTimeIntervalNotificationTrigger *trigger = nil; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger]; [center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) { if (error) { NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; - notificationResponse(channelID, false, [errorMsg UTF8String]); + captureResult(channelID, false, [errorMsg UTF8String]); } else { - notificationResponse(channelID, true, NULL); + captureResult(channelID, true, NULL); } }]; } @@ -219,7 +219,7 @@ void registerNotificationCategory(int channelID, const char *categoryId, const c if (error) { NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]]; - notificationResponse(channelID, false, [errorMsg UTF8String]); + captureResult(channelID, false, [errorMsg UTF8String]); return; } @@ -281,11 +281,11 @@ void registerNotificationCategory(int channelID, const char *categoryId, const c [updatedCategories addObject:newCategory]; [center setNotificationCategories:updatedCategories]; - notificationResponse(channelID, true, NULL); + captureResult(channelID, true, NULL); }]; } -void removeNotificationCategory(const char *categoryId) { +void removeNotificationCategory(int channelID, const char *categoryId) { NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; @@ -304,6 +304,10 @@ void removeNotificationCategory(const char *categoryId) { if (categoryToRemove) { [updatedCategories removeObject:categoryToRemove]; [center setNotificationCategories:updatedCategories]; + captureResult(channelID, true, NULL); + } else { + NSString *errorMsg = [NSString stringWithFormat:@"Category '%@' not found", nsCategoryId]; + captureResult(channelID, false, [errorMsg UTF8String]); } }]; } diff --git a/v3/pkg/services/notifications/notifications_linux.go b/v3/pkg/services/notifications/notifications_linux.go index 68217d861..34b110987 100644 --- a/v3/pkg/services/notifications/notifications_linux.go +++ b/v3/pkg/services/notifications/notifications_linux.go @@ -1,7 +1,5 @@ //go:build linux -// WIP - WILL NOT WORK PROPERLY - package notifications import ( @@ -69,11 +67,14 @@ type notificationContext struct { type internalNotifier struct { sync.Mutex - method string - dbusConn *dbus.Conn - sendPath string - activeNotifs map[string]uint32 // Maps our notification IDs to system IDs - contexts map[string]*notificationContext // Stores notification contexts by our ID + method string + dbusConn *dbus.Conn + sendPath string + activeNotifs map[string]uint32 // Maps our notification IDs to system IDs + contexts map[string]*notificationContext // Stores notification contexts by our ID + listenerCtx context.Context + listenerCancel context.CancelFunc + listenerRunning bool } var notifier *internalNotifier @@ -107,10 +108,33 @@ func (ns *Service) ServiceStartup(ctx context.Context, options application.Servi return err } +func (n *internalNotifier) shutdown() { + n.Lock() + defer n.Unlock() + + // Cancel the listener context if it's running + if n.listenerCancel != nil { + n.listenerCancel() + n.listenerCancel = nil + } + + // Close the connection + if n.dbusConn != nil { + n.dbusConn.Close() + n.dbusConn = nil + } + + // Clear state + n.activeNotifs = make(map[string]uint32) + n.contexts = make(map[string]*notificationContext) + n.method = "none" + n.sendPath = "" +} + // ServiceShutdown is called when the service is unloaded func (ns *Service) ServiceShutdown() error { - if notifier != nil && notifier.dbusConn != nil { - notifier.dbusConn.Close() + if notifier != nil { + notifier.shutdown() } return saveCategories() } @@ -119,6 +143,19 @@ func (ns *Service) ServiceShutdown() error { func (n *internalNotifier) init() error { var err error + // Cancel any existing listener before starting a new one + if n.listenerCancel != nil { + n.listenerCancel() + } + + // Create a new context for the listener + n.listenerCtx, n.listenerCancel = context.WithCancel(context.Background()) + + // Reset state + n.activeNotifs = make(map[string]uint32) + n.contexts = make(map[string]*notificationContext) + n.listenerRunning = false + checkDbus := func() (*dbus.Conn, error) { conn, err := dbus.SessionBusPrivate() if err != nil { @@ -161,8 +198,9 @@ func (n *internalNotifier) init() error { n.dbusConn, err = checkDbus() if err == nil { n.method = MethodDbus - // Start the dbus signal listener - go n.startDBusListener() + // Start the dbus signal listener with context + go n.startDBusListener(n.listenerCtx) + n.listenerRunning = true return nil } if n.dbusConn != nil { @@ -194,24 +232,44 @@ func (n *internalNotifier) init() error { } // startDBusListener listens for DBus signals for notification actions and closures -func (n *internalNotifier) startDBusListener() { +func (n *internalNotifier) startDBusListener(ctx context.Context) { signal := make(chan *dbus.Signal, notifyChannelBufferSize) n.dbusConn.Signal(signal) - for s := range signal { - if s == nil || len(s.Body) < 2 { - continue - } + defer func() { + n.Lock() + n.listenerRunning = false + n.Unlock() + n.dbusConn.RemoveSignal(signal) // Remove signal handler + close(signal) // Clean up channel + }() - switch s.Name { - case signalNotificationClosed: - systemID := s.Body[0].(uint32) - reason := closedReason(s.Body[1].(uint32)).string() - n.handleNotificationClosed(systemID, reason) - case signalActionInvoked: - systemID := s.Body[0].(uint32) - actionKey := s.Body[1].(string) - n.handleActionInvoked(systemID, actionKey) + for { + select { + case <-ctx.Done(): + // Context was cancelled, exit gracefully + return + + case s := <-signal: + if s == nil { + // Channel closed or nil signal + continue + } + + if len(s.Body) < 2 { + continue + } + + switch s.Name { + case signalNotificationClosed: + systemID := s.Body[0].(uint32) + reason := closedReason(s.Body[1].(uint32)).string() + n.handleNotificationClosed(systemID, reason) + case signalActionInvoked: + systemID := s.Body[0].(uint32) + actionKey := s.Body[1].(string) + n.handleActionInvoked(systemID, actionKey) + } } } } diff --git a/v3/pkg/services/notifications/notifications_windows.go b/v3/pkg/services/notifications/notifications_windows.go index 10e0f01a0..6afda46b4 100644 --- a/v3/pkg/services/notifications/notifications_windows.go +++ b/v3/pkg/services/notifications/notifications_windows.go @@ -305,7 +305,7 @@ func parseNotificationResponse(response string) (action string, data string) { func saveIconToDir() error { icon, err := application.NewIconFromResource(w32.GetModuleHandle(""), uint16(3)) if err != nil { - return fmt.Errorf("failed to retrieve application icon") + return fmt.Errorf("failed to retrieve application icon: %w", err) } return saveHIconAsPNG(icon, iconPath)