mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
better interop with obj-c
This commit is contained in:
parent
3bdb3ddba3
commit
a807c269cc
5 changed files with 183 additions and 74 deletions
|
|
@ -1,12 +1,17 @@
|
|||
package notifications
|
||||
|
||||
import "sync"
|
||||
|
||||
// Service represents the notifications service
|
||||
type Service struct {
|
||||
// Callback is called when a notification response is received
|
||||
Callback func(response NotificationResponse)
|
||||
// notificationResponseCallback is called when a notification response is received
|
||||
notificationResponseCallback func(response NotificationResponse)
|
||||
|
||||
callbackLock sync.RWMutex
|
||||
}
|
||||
|
||||
var NotificationService *Service
|
||||
var notificationServiceLock sync.RWMutex
|
||||
|
||||
// NotificationAction represents an action button for a notification
|
||||
type NotificationAction = struct {
|
||||
|
|
@ -54,18 +59,22 @@ func (ns *Service) ServiceName() string {
|
|||
}
|
||||
|
||||
// OnNotificationResponse registers a callback function that will be called when
|
||||
// a notification response is received from the user
|
||||
// a notification response is received from the user.
|
||||
func (ns *Service) OnNotificationResponse(callback func(response NotificationResponse)) {
|
||||
if ns.Callback != nil {
|
||||
return
|
||||
}
|
||||
ns.Callback = callback
|
||||
ns.callbackLock.Lock()
|
||||
defer ns.callbackLock.Unlock()
|
||||
|
||||
ns.notificationResponseCallback = callback
|
||||
}
|
||||
|
||||
// handleNotificationResponse is an internal method to handle notification responses
|
||||
// and invoke the registered callback if one exists
|
||||
func (ns *Service) handleNotificationResponse(response NotificationResponse) {
|
||||
if ns.Callback != nil {
|
||||
ns.Callback(response)
|
||||
ns.callbackLock.RLock()
|
||||
callback := ns.notificationResponseCallback
|
||||
ns.callbackLock.RUnlock()
|
||||
|
||||
if callback != nil {
|
||||
callback(response)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,16 +12,32 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
var AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
|
||||
type notificationChannel struct {
|
||||
authorized bool
|
||||
err error
|
||||
}
|
||||
|
||||
var (
|
||||
notificationChannels = make(map[int]chan notificationChannel)
|
||||
notificationChannelsLock sync.Mutex
|
||||
nextChannelID int
|
||||
)
|
||||
|
||||
const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
|
||||
const BundleIdentifierError = fmt.Errorf("notifications require a bundled application with a unique bundle identifier")
|
||||
|
||||
// Creates a new Notifications Service.
|
||||
// Your app must be packaged and signed for this feature to work.
|
||||
func New() *Service {
|
||||
notificationServiceLock.Lock()
|
||||
defer notificationServiceLock.Unlock()
|
||||
|
||||
if NotificationService == nil {
|
||||
NotificationService = &Service{}
|
||||
}
|
||||
|
|
@ -31,7 +47,7 @@ func New() *Service {
|
|||
// ServiceStartup is called when the service is loaded.
|
||||
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
if !CheckBundleIdentifier() {
|
||||
return fmt.Errorf("notifications require a bundled application with a unique bundle identifier")
|
||||
return BundleIdentifierError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -45,21 +61,32 @@ func CheckBundleIdentifier() bool {
|
|||
return bool(C.checkBundleIdentifier())
|
||||
}
|
||||
|
||||
// RequestUserNotificationAuthorization requests permission for notifications.
|
||||
func (ns *Service) RequestUserNotificationAuthorization() (bool, error) {
|
||||
// RequestNotificationAuthorization requests permission for notifications.
|
||||
func (ns *Service) RequestNotificationAuthorization() (bool, err) {
|
||||
if !CheckBundleIdentifier() {
|
||||
return false, fmt.Errorf("notifications require a bundled application with a unique bundle identifier")
|
||||
return false, BundleIdentifierError
|
||||
}
|
||||
result := C.requestUserNotificationAuthorization(nil)
|
||||
return result == true, nil
|
||||
|
||||
id, resultCh := registerChannel()
|
||||
|
||||
C.requestNotificationAuthorization(C.int(id))
|
||||
|
||||
result := <-resultCh
|
||||
return result.authorized, result.err
|
||||
}
|
||||
|
||||
// CheckNotificationAuthorization checks current notification permission status.
|
||||
func (ns *Service) CheckNotificationAuthorization() (bool, error) {
|
||||
func (ns *Service) CheckNotificationAuthorization() (bool, err) {
|
||||
if !CheckBundleIdentifier() {
|
||||
return false, fmt.Errorf("notifications require a bundled application with a unique bundle identifier")
|
||||
return false, BundleIdentifierError
|
||||
}
|
||||
return bool(C.checkNotificationAuthorization()), nil
|
||||
|
||||
id, resultCh := registerChannel()
|
||||
|
||||
C.checkNotificationAuthorization(C.int(id))
|
||||
|
||||
result := <-resultCh
|
||||
return result.authorized, result.err
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||
|
|
@ -212,9 +239,48 @@ func (ns *Service) RemoveNotification(identifier string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// SetIcon is a macOS stub. The application icon is used automatically.
|
||||
func (ns *Service) SetIcon(_ []byte) {
|
||||
return
|
||||
//export requestNotificationAuthorizationResponse
|
||||
func requestNotificationAuthorizationResponse(channelID C.int, authorized C.bool, errorMsg *C.char) {
|
||||
resultCh, exists := getAndDeleteChannel(int(channelID))
|
||||
if !exists {
|
||||
// handle this
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if errorMsg != nil {
|
||||
err = fmt.Errorf("%s", C.GoString(errorMsg))
|
||||
C.free(unsafe.Pointer(errorMsg))
|
||||
}
|
||||
|
||||
resultCh <- notificationChannel{
|
||||
authorized: bool(authorized),
|
||||
err: err,
|
||||
}
|
||||
|
||||
close(resultCh)
|
||||
}
|
||||
|
||||
//export checkNotificationAuthorizationResponse
|
||||
func checkNotificationAuthorizationResponse(channelID C.int, authorized C.bool, errorMsg *C.char) {
|
||||
resultCh, exists := getAndDeleteChannel(int(channelID))
|
||||
if !exists {
|
||||
// handle this
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if errorMsg != nil {
|
||||
err = fmt.Errorf("%s", C.GoString(errorMsg))
|
||||
C.free(unsafe.Pointer(errorMsg))
|
||||
}
|
||||
|
||||
resultCh <- notificationChannel{
|
||||
authorized: bool(authorized),
|
||||
err: err,
|
||||
}
|
||||
|
||||
close(resultCh)
|
||||
}
|
||||
|
||||
//export didReceiveNotificationResponse
|
||||
|
|
@ -230,7 +296,44 @@ func didReceiveNotificationResponse(jsonPayload *C.char) {
|
|||
response.ActionIdentifier = DefaultActionIdentifier
|
||||
}
|
||||
|
||||
if NotificationService != nil {
|
||||
NotificationService.handleNotificationResponse(response)
|
||||
notificationServiceLock.RLock()
|
||||
ns := NotificationService
|
||||
notificationServiceLock.RUnlock()
|
||||
|
||||
if service != nil {
|
||||
ns.callbackLock.RLock()
|
||||
callback := ns.notificationResponseCallback
|
||||
ns.callbackLock.RUnlock()
|
||||
|
||||
if callback != nil {
|
||||
callback(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func registerChannel() (int, chan notificationChannel) {
|
||||
notificationChannelsLock.Lock()
|
||||
defer notificationChannelsLock.Unlock()
|
||||
|
||||
id := nextChannelID
|
||||
nextChannelID++
|
||||
|
||||
resultCh := make(chan notificationChannel{
|
||||
authorized bool
|
||||
err error
|
||||
}, 1)
|
||||
|
||||
notificationChannels[id] = resultCh
|
||||
return id, resultCh
|
||||
}
|
||||
|
||||
func getChannel(id int) (chan notificationChannel, bool) {
|
||||
notificationChannelsLock.Lock()
|
||||
defer notificationChannelsLock.Unlock()
|
||||
|
||||
ch, exists := notificationChannels[id]
|
||||
if exists {
|
||||
delete(notificationChannels, id)
|
||||
}
|
||||
return ch, exists
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
bool checkBundleIdentifier(void);
|
||||
bool requestUserNotificationAuthorization(void *completion);
|
||||
bool checkNotificationAuthorization(void);
|
||||
bool requestNotificationAuthorization(void *channelPtr);
|
||||
bool checkNotificationAuthorization(void *channelPtr);
|
||||
void sendNotification(const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json, void *completion);
|
||||
void sendNotificationWithActions(const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json, void *completion);
|
||||
void registerNotificationCategory(const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle);
|
||||
|
|
@ -17,4 +17,8 @@ void removePendingNotification(const char *identifier);
|
|||
void removeAllDeliveredNotifications(void);
|
||||
void removeDeliveredNotification(const char *identifier);
|
||||
|
||||
extern void requestNotificationAuthorizationResponse(int channelID, bool authorized, const char* error);
|
||||
extern void checkNotificationAuthorizationResponse(int channelID, bool authorized, const char* error);
|
||||
extern void didReceiveNotificationResponse(const char *jsonPayload);
|
||||
|
||||
#endif /* NOTIFICATIONS_DARWIN_H */
|
||||
|
|
@ -2,8 +2,6 @@
|
|||
#import <Cocoa/Cocoa.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
extern void didReceiveNotificationResponse(const char *jsonPayload);
|
||||
|
||||
@interface NotificationsDelegate : NSObject <UNUserNotificationCenterDelegate>
|
||||
@end
|
||||
|
||||
|
|
@ -71,8 +69,8 @@ static void ensureDelegateInitialized(void) {
|
|||
bool checkBundleIdentifier(void) {
|
||||
NSBundle *main = [NSBundle mainBundle];
|
||||
if (main.bundleIdentifier == nil) {
|
||||
NSLog(@"Error: Cannot use notifications in development mode.\n"
|
||||
" Notifications require the app to be properly bundled with a bundle identifier.\n"
|
||||
NSLog(@"Error: Cannot use the notification API in development mode.\n"
|
||||
" Notifications require the app to be properly bundled with a bundle identifier and signed.\n"
|
||||
" To test notifications:\n"
|
||||
" 1. Build and package your app using 'wails3 package'\n"
|
||||
" 2. Sign the packaged .app\n"
|
||||
|
|
@ -82,40 +80,32 @@ bool checkBundleIdentifier(void) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool requestUserNotificationAuthorization(void *completion) {
|
||||
if (!checkBundleIdentifier()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool requestNotificationAuthorization(int channelID) {
|
||||
ensureDelegateInitialized();
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
|
||||
|
||||
[center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (completion != NULL) {
|
||||
void (^callback)(NSError *, BOOL) = completion;
|
||||
callback(error, granted);
|
||||
if (error) {
|
||||
requestNotificationAuthorizationResponse(channelID, false, [[error localizedDescription] UTF8String]);
|
||||
} else {
|
||||
requestNotificationAuthorizationResponse(channelID, granted, NULL);
|
||||
}
|
||||
}];
|
||||
return true;
|
||||
}
|
||||
|
||||
bool checkNotificationAuthorization(void) {
|
||||
bool checkNotificationAuthorization(int channelID) {
|
||||
ensureDelegateInitialized();
|
||||
|
||||
__block BOOL isAuthorized = NO;
|
||||
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
|
||||
isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized);
|
||||
dispatch_semaphore_signal(semaphore);
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||
BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized);
|
||||
checkNotificationAuthorizationResponse(channelID, isAuthorized, NULL);
|
||||
}];
|
||||
|
||||
// Wait for response with a timeout
|
||||
dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
|
||||
return isAuthorized;
|
||||
}
|
||||
|
||||
void sendNotification(const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json, void *completion) {
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
NotificationLock sync.RWMutex
|
||||
NotificationCategories = make(map[string]NotificationCategory)
|
||||
AppName string
|
||||
AppGUID string
|
||||
IconPath string
|
||||
NotificationCategories = make(map[string]NotificationCategory)
|
||||
notificationCategoriesLock sync.RWMutex
|
||||
appName string
|
||||
appGUID string
|
||||
iconPath string
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -42,6 +42,9 @@ type NotificationPayload struct {
|
|||
|
||||
// Creates a new Notifications Service.
|
||||
func New() *Service {
|
||||
notificationServiceLock.Lock()
|
||||
defer notificationServiceLock.Unlock()
|
||||
|
||||
if NotificationService == nil {
|
||||
NotificationService = &Service{}
|
||||
}
|
||||
|
|
@ -51,20 +54,20 @@ func New() *Service {
|
|||
// ServiceStartup is called when the service is loaded
|
||||
// Sets an activation callback to emit an event when notifications are interacted with.
|
||||
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
AppName = application.Get().Config().Name
|
||||
appName = application.Get().Config().Name
|
||||
|
||||
guid, err := getGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
AppGUID = guid
|
||||
appGUID = guid
|
||||
|
||||
IconPath = filepath.Join(os.TempDir(), AppName+guid+".png")
|
||||
iconPath = filepath.Join(os.TempDir(), appName+appGUID+".png")
|
||||
|
||||
toast.SetAppData(toast.AppData{
|
||||
AppID: AppName,
|
||||
AppID: appName,
|
||||
GUID: guid,
|
||||
IconPath: IconPath,
|
||||
IconPath: iconPath,
|
||||
})
|
||||
|
||||
toast.SetActivationCallback(func(args string, data []toast.UserData) {
|
||||
|
|
@ -147,9 +150,9 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
|
|||
fmt.Printf("Error saving icon: %v\n", err)
|
||||
}
|
||||
|
||||
NotificationLock.RLock()
|
||||
notificationCategoriesLock.RLock()
|
||||
nCategory := NotificationCategories[options.CategoryID]
|
||||
NotificationLock.RUnlock()
|
||||
notificationCategoriesLock.RUnlock()
|
||||
|
||||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
|
|
@ -195,7 +198,7 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
|
|||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
// Registering a category with the same name as a previously registered NotificationCategory will override it.
|
||||
func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error {
|
||||
NotificationLock.Lock()
|
||||
notificationCategoriesLock.Lock()
|
||||
NotificationCategories[category.ID] = NotificationCategory{
|
||||
ID: category.ID,
|
||||
Actions: category.Actions,
|
||||
|
|
@ -203,16 +206,16 @@ func (ns *Service) RegisterNotificationCategory(category NotificationCategory) e
|
|||
ReplyPlaceholder: category.ReplyPlaceholder,
|
||||
ReplyButtonTitle: category.ReplyButtonTitle,
|
||||
}
|
||||
NotificationLock.Unlock()
|
||||
notificationCategoriesLock.Unlock()
|
||||
|
||||
return saveCategoriesToRegistry()
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory removes a previously registered NotificationCategory.
|
||||
func (ns *Service) RemoveNotificationCategory(categoryId string) error {
|
||||
NotificationLock.Lock()
|
||||
notificationCategoriesLock.Lock()
|
||||
delete(NotificationCategories, categoryId)
|
||||
NotificationLock.Unlock()
|
||||
notificationCategoriesLock.Unlock()
|
||||
|
||||
return saveCategoriesToRegistry()
|
||||
}
|
||||
|
|
@ -298,11 +301,11 @@ func saveIconToDir() error {
|
|||
return fmt.Errorf("failed to retrieve application icon")
|
||||
}
|
||||
|
||||
return saveHIconAsPNG(icon, IconPath)
|
||||
return saveHIconAsPNG(icon, iconPath)
|
||||
}
|
||||
|
||||
func saveCategoriesToRegistry() error {
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName)
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
|
||||
|
||||
key, _, err := registry.CreateKey(
|
||||
registry.CURRENT_USER,
|
||||
|
|
@ -314,9 +317,9 @@ func saveCategoriesToRegistry() error {
|
|||
}
|
||||
defer key.Close()
|
||||
|
||||
NotificationLock.RLock()
|
||||
notificationCategoriesLock.RLock()
|
||||
data, err := json.Marshal(NotificationCategories)
|
||||
NotificationLock.RUnlock()
|
||||
notificationCategoriesLock.RUnlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -325,7 +328,7 @@ func saveCategoriesToRegistry() error {
|
|||
}
|
||||
|
||||
func loadCategoriesFromRegistry() error {
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName)
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, appName)
|
||||
|
||||
key, err := registry.OpenKey(
|
||||
registry.CURRENT_USER,
|
||||
|
|
@ -350,9 +353,9 @@ func loadCategoriesFromRegistry() error {
|
|||
return err
|
||||
}
|
||||
|
||||
NotificationLock.Lock()
|
||||
notificationCategoriesLock.Lock()
|
||||
NotificationCategories = categories
|
||||
NotificationLock.Unlock()
|
||||
notificationCategoriesLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -367,7 +370,7 @@ func getUserText(data []toast.UserData) (string, bool) {
|
|||
}
|
||||
|
||||
func getGUID() (string, error) {
|
||||
keyPath := ToastRegistryPath + AppName
|
||||
keyPath := ToastRegistryPath + appName
|
||||
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
|
||||
if err == nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue