mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
coderabbit impr
This commit is contained in:
parent
e4e24a0f00
commit
b60ef7729e
7 changed files with 156 additions and 61 deletions
|
|
@ -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]);
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue