coderabbit impr

This commit is contained in:
popaprozac 2025-03-18 22:46:44 -07:00
commit b60ef7729e
7 changed files with 156 additions and 61 deletions

View file

@ -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]);
});

View file

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

View file

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

View file

@ -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);

View file

@ -2,7 +2,7 @@
#import <Cocoa/Cocoa.h>
#import <UserNotifications/UserNotifications.h>
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 <UNUserNotificationCenterDelegate>
@ -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]);
}
}];
}

View file

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

View file

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