Skip to content

[v3] Notifications API #4098

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 101 commits into from
Apr 13, 2025
Merged
Changes from 1 commit
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
32839bd
initial notification impl
popaprozac Feb 20, 2025
274511c
update log message
popaprozac Feb 21, 2025
f694ad2
service and minor refactor
popaprozac Feb 22, 2025
cc524d7
update example
popaprozac Feb 22, 2025
87f15ce
refactor
popaprozac Feb 22, 2025
91f1704
cleanup
popaprozac Feb 22, 2025
01fa307
windows impl
popaprozac Feb 23, 2025
c7d54e2
remove app event
popaprozac Feb 23, 2025
7757eae
standalone example
popaprozac Feb 23, 2025
9f39577
add comments
popaprozac Feb 23, 2025
47a0977
add overwrite and remove notification category
popaprozac Feb 23, 2025
6d44068
save notification categories to registry
popaprozac Feb 23, 2025
ab9c460
quick fixes
popaprozac Feb 23, 2025
27e512d
add data option to basic notification
popaprozac Feb 23, 2025
f8647ff
update min macos version
popaprozac Feb 23, 2025
c1230d4
update win impl to match
popaprozac Feb 23, 2025
9eab58f
update comments
popaprozac Feb 23, 2025
cc760a1
test data with toast
popaprozac Feb 23, 2025
978a982
don't add title or body quite yet
popaprozac Feb 23, 2025
bf4222c
update example
popaprozac Feb 24, 2025
c904433
update example
popaprozac Feb 24, 2025
983c25f
rw safety
popaprozac Feb 24, 2025
1e3511c
fix deadlock
popaprozac Feb 24, 2025
3dbf058
update comments
popaprozac Feb 24, 2025
be716fc
fix input id
popaprozac Feb 25, 2025
8da9981
for review
popaprozac Feb 25, 2025
e8ad42a
switch to callback style
popaprozac Feb 25, 2025
8bbe381
Merge branch 'v3-alpha' into notifications_darwin
popaprozac Feb 25, 2025
67adc45
update example
popaprozac Feb 25, 2025
253f672
init linux
popaprozac Feb 25, 2025
135defc
init test linux
popaprozac Feb 25, 2025
7bbc275
fix windows
popaprozac Feb 25, 2025
fa498f1
encode user data on windows
popaprozac Feb 25, 2025
050e4ef
fix default complex action
popaprozac Feb 25, 2025
60c1a86
update example
popaprozac Feb 25, 2025
10ae1f6
export config from app
popaprozac Feb 25, 2025
a4c4ab6
add id to macOS payload
popaprozac Feb 25, 2025
acad85e
update example
popaprozac Feb 25, 2025
60ae0c1
test linux userInfo
popaprozac Feb 26, 2025
47e5b10
add debug
popaprozac Feb 26, 2025
5ad3d73
wrangle variants
popaprozac Feb 26, 2025
e86cf68
linux?
popaprozac Feb 26, 2025
db2cab7
grab icon from app for notifications on windows
popaprozac Feb 26, 2025
e216b3e
set icon
popaprozac Feb 26, 2025
207b162
Merge branch 'notifications_darwin' of github.com:popaprozac/wails in…
popaprozac Feb 26, 2025
3bdb3dd
use app icon
popaprozac Feb 26, 2025
a807c26
better interop with obj-c
popaprozac Feb 27, 2025
ad74972
can you tell i'm not a go programmer?
popaprozac Feb 27, 2025
d9af6de
ignore bindings notification callback
popaprozac Mar 3, 2025
833d8ec
refactor macos
popaprozac Mar 5, 2025
e4ec06a
update example
Mar 14, 2025
1e67e94
rename auth func
popaprozac Mar 15, 2025
1fa6894
//
popaprozac Mar 15, 2025
e46abdc
update example
popaprozac Mar 15, 2025
2cb0dac
set default action id on MATE
popaprozac Mar 15, 2025
6d15f08
Merge branch 'v3-alpha' into notifications_darwin
popaprozac Mar 15, 2025
e810d6a
tidy
popaprozac Mar 15, 2025
e4e24a0
clean
popaprozac Mar 19, 2025
b60ef77
coderabbit impr
popaprozac Mar 19, 2025
ee885fe
fix mem crash
popaprozac Mar 19, 2025
4af058b
more cleanup
popaprozac Mar 20, 2025
58b5c10
Merge branch 'v3-alpha' into notifications_darwin
popaprozac Mar 20, 2025
25256f7
allow only one notification service per app
popaprozac Mar 20, 2025
d98f528
Merge branch 'notifications_darwin' of github.com:popaprozac/wails in…
popaprozac Mar 20, 2025
bd85738
fix err message
popaprozac Mar 20, 2025
c58709c
lock registry access/writes
popaprozac Mar 20, 2025
b1db2bb
add missing error
popaprozac Mar 20, 2025
88f1336
reorg platform impl
popaprozac Mar 20, 2025
94f245f
bug fix
popaprozac Mar 20, 2025
e55bf8d
linux bug fix
popaprozac Mar 20, 2025
9adeef2
fix linux and remove dup code
popaprozac Mar 20, 2025
5dc7bd9
refactor/simplify linux
popaprozac Mar 20, 2025
be1da68
avoid deadlock on win
popaprozac Mar 21, 2025
ef3b7d2
pass service options
popaprozac Mar 22, 2025
81b40d2
ensure exactly one delegate created
popaprozac Mar 22, 2025
f6a5bed
windows err handling
popaprozac Mar 22, 2025
b2b8f14
windows err handling
popaprozac Mar 22, 2025
f870dcc
handle notif content errors
popaprozac Mar 23, 2025
9948cee
monitor dbus connection
popaprozac Mar 23, 2025
ace4f70
fix channel cleanup linux
popaprozac Mar 23, 2025
448c78b
rewrite and simplify linux impl
popaprozac Mar 23, 2025
5ddb27f
move chan creation on macos and handle linux shutdown
popaprozac Mar 23, 2025
a683918
remove deadlock
popaprozac Mar 23, 2025
9a45423
follow dbus spec
popaprozac Mar 24, 2025
7f496c9
fix linux note
popaprozac Mar 24, 2025
1f82166
ensure cat id is set for macOS and other rabbit suggestions
popaprozac Mar 24, 2025
b102279
update example
popaprozac Mar 24, 2025
d25f923
wait on dbus
popaprozac Mar 25, 2025
276c653
update example
popaprozac Mar 25, 2025
7d0f7f4
revert linux wait and update payload on win
popaprozac Mar 25, 2025
af5abda
update example
popaprozac Mar 25, 2025
1db65a8
stage docs
popaprozac Mar 27, 2025
743882a
update docs/changelog
popaprozac Mar 27, 2025
1022f47
fix docs
popaprozac Mar 27, 2025
bd5c82e
fix docs
popaprozac Mar 27, 2025
b313f2c
fix docs
popaprozac Mar 27, 2025
9a41b0c
move icon method, impl interface, check compat, don't panic!
popaprozac Mar 30, 2025
75dbe85
remove comptime check
popaprozac Mar 30, 2025
0b546a1
Merge branch 'v3-alpha' into notifications_darwin
leaanthony Apr 12, 2025
be1d500
Update v3/examples/notifications/build/windows/nsis/wails_tools.nsh
leaanthony Apr 12, 2025
a8e91e7
Merge branch 'v3-alpha' into notifications_darwin
leaanthony Apr 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
more cleanup
  • Loading branch information
popaprozac committed Mar 20, 2025
commit 4af058bd02ee74d16a8b80de9f54cba12a8c1a1a
31 changes: 29 additions & 2 deletions v3/pkg/services/notifications/notifications.go
Original file line number Diff line number Diff line change
@@ -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
}
12 changes: 10 additions & 2 deletions v3/pkg/services/notifications/notifications_darwin.go
Original file line number Diff line number Diff line change
@@ -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():
71 changes: 29 additions & 42 deletions v3/pkg/services/notifications/notifications_darwin.m
Original file line number Diff line number Diff line change
@@ -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;
}
}

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
return content;
}

void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
ensureDelegateInitialized();

UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = nsTitle;
content.subtitle = nsSubtitle;
content.body = nsBody;
content.sound = [UNNotificationSound defaultSound];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

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

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];

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];

UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
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];
content.categoryIdentifier = nsCategoryId;

if (customData.count > 0) {
8 changes: 8 additions & 0 deletions v3/pkg/services/notifications/notifications_linux.go
Original file line number Diff line number Diff line change
@@ -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()
36 changes: 25 additions & 11 deletions v3/pkg/services/notifications/notifications_windows.go
Original file line number Diff line number Diff line change
@@ -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()