#import "notify.h"
static const NSTimeInterval kDeliveryTimeoutSecs = 2.0;
NSString* getBundleIdentifier(NSString* appName) {
NSString* findString = [NSString stringWithFormat:@"get id of application \"%@\"", appName];
NSAppleScript* findScript = [[NSAppleScript alloc] initWithSource:findString];
NSAppleEventDescriptor* resultDescriptor = [findScript executeAndReturnError:nil];
return [resultDescriptor stringValue];
}
BOOL setApplication(NSString* newbundleIdentifier) {
@autoreleasepool {
if (!installNSBundleHook()) {
return NO;
}
if (LSCopyApplicationURLsForBundleIdentifier((CFStringRef)newbundleIdentifier, NULL) != NULL) {
[fakeBundleIdentifier release];
fakeBundleIdentifier = newbundleIdentifier;
[newbundleIdentifier retain];
return YES;
}
return NO;
}
}
NSImage* getImageFromURL(NSString* url) {
NSURL* imageURL = [NSURL URLWithString:url];
if ([[imageURL scheme] length] == 0) {
imageURL = [NSURL fileURLWithPath:url];
}
return [[NSImage alloc] initWithContentsOfURL:imageURL];
}
static void uuidBytesFromNotification(NSUserNotification* n, unsigned char out[16]) {
NSUUID* uuid = n.identifier ? [[NSUUID alloc] initWithUUIDString:n.identifier] : nil;
if (uuid) {
[uuid getUUIDBytes:out];
[uuid release];
} else {
memset(out, 0, 16);
}
}
static void resolveAutoDismiss(const unsigned char* uuid) {
if (rust_notification_is_done(uuid))
return; [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
if (rust_notification_is_done(uuid))
return;
rust_notification_auto_dismissed(uuid);
}
void sendNotification(NSString* title, NSString* subtitle, NSString* message, NSDictionary* options, const unsigned char* notificationId, BOOL shouldWait) {
@autoreleasepool {
NSUserNotificationCenter* notificationCenter = [NSUserNotificationCenter defaultUserNotificationCenter];
NSUserNotification* userNotification = [[NSUserNotification alloc] init];
BOOL isScheduled = NO;
NSUUID* uuid = [[NSUUID alloc] initWithUUIDBytes:notificationId];
NSString* identifierString = [uuid UUIDString];
userNotification.identifier = identifierString;
[uuid release];
userNotification.title = title;
if (![subtitle isEqualToString:@""]) {
userNotification.subtitle = subtitle;
}
userNotification.informativeText = message;
if (options[@"sound"] && ![options[@"sound"] isEqualToString:@""]) {
if ([options[@"sound"] isEqualToString:@"NSUserNotificationDefaultSoundName"]) {
userNotification.soundName = NSUserNotificationDefaultSoundName;
} else {
userNotification.soundName = options[@"sound"];
}
}
if (options[@"deliveryDate"] && ![options[@"deliveryDate"] isEqualToString:@""]) {
double deliveryDate = [options[@"deliveryDate"] doubleValue];
userNotification.deliveryDate = [NSDate dateWithTimeIntervalSince1970:deliveryDate];
isScheduled = YES;
}
if (options[@"mainButtonLabel"] && ![options[@"mainButtonLabel"] isEqualToString:@""]) {
userNotification.actionButtonTitle = options[@"mainButtonLabel"];
userNotification.hasActionButton = 1;
} else {
userNotification.hasActionButton = 0;
}
if (options[@"actions"] && ![options[@"actions"] isEqualToString:@""]) {
[userNotification setValue:@YES forKey:@"_showsButtons"];
NSArray* myActions = [options[@"actions"] componentsSeparatedByString:@","];
if (myActions.count > 1) {
[userNotification setValue:@YES forKey:@"_alwaysShowAlternateActionMenu"];
[userNotification setValue:myActions forKey:@"_alternateActionButtonTitles"];
}
}
if (options[@"closeButtonLabel"] && ![options[@"closeButtonLabel"] isEqualToString:@""]) {
[userNotification setValue:@YES forKey:@"_showsButtons"];
userNotification.otherButtonTitle = options[@"closeButtonLabel"];
}
if (options[@"response"] && ![options[@"response"] isEqualToString:@""]) {
userNotification.hasReplyButton = 1;
userNotification.responsePlaceholder = options[@"mainButtonLabel"];
}
if (options[@"appIcon"] && ![options[@"appIcon"] isEqualToString:@""]) {
NSImage* icon = getImageFromURL(options[@"appIcon"]);
[userNotification setValue:icon forKey:@"_identityImage"];
[userNotification setValue:@(false) forKey:@"_identityImageHasBorder"];
}
if (options[@"contentImage"] && ![options[@"contentImage"] isEqualToString:@""]) {
userNotification.contentImage = getImageFromURL(options[@"contentImage"]);
}
if (isScheduled) {
[notificationCenter scheduleNotification:userNotification];
} else {
[notificationCenter deliverNotification:userNotification];
}
if (!shouldWait) {
if (!isScheduled) {
if ([NSThread isMainThread]) {
NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow:kDeliveryTimeoutSecs];
while (!rust_notification_is_delivered(notificationId) &&
[deadline timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop]
runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
}
} else {
rust_wait_for_delivery(notificationId);
}
}
return;
}
BOOL (^wasAutoDismissed)(void) = ^BOOL {
for (NSUserNotification* n in notificationCenter.deliveredNotifications) {
if ([n.identifier isEqualToString:identifierString])
return NO;
}
return YES;
};
if ([NSThread isMainThread]) {
NSDate* deliveryDeadline = [NSDate dateWithTimeIntervalSinceNow:kDeliveryTimeoutSecs];
while (!rust_notification_is_delivered(notificationId) && !rust_notification_is_done(notificationId) &&
[deliveryDeadline timeIntervalSinceNow] > 0) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
}
if (!rust_notification_is_done(notificationId) && !rust_notification_is_delivered(notificationId))
resolveAutoDismiss(notificationId);
while (!rust_notification_is_done(notificationId)) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
if (rust_notification_is_delivered(notificationId) && wasAutoDismissed())
resolveAutoDismiss(notificationId);
}
} else {
NSData* notificationIdData = [NSData dataWithBytes:notificationId length:16];
NSDate* pollStarted = [NSDate date];
NSTimer* dismissPoll = [NSTimer timerWithTimeInterval:0.5
repeats:YES
block:^(NSTimer* t) {
if (!rust_notification_is_delivered(notificationIdData.bytes)) {
if (!rust_notification_is_done(notificationIdData.bytes) && -[pollStarted timeIntervalSinceNow] > kDeliveryTimeoutSecs) {
[t invalidate];
resolveAutoDismiss(notificationIdData.bytes);
}
return;
}
if (wasAutoDismissed()) {
[t invalidate];
resolveAutoDismiss(notificationIdData.bytes);
}
}];
[[NSRunLoop mainRunLoop] addTimer:dismissPoll forMode:NSDefaultRunLoopMode];
rust_wait_for_notification(notificationId);
dispatch_async(dispatch_get_main_queue(), ^{
[dismissPoll invalidate];
});
}
}
}
@implementation NotificationCenterDelegate
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didDeliverNotification:(NSUserNotification*)notification {
unsigned char bytes[16];
uuidBytesFromNotification(notification, bytes);
rust_notification_delivered(bytes);
}
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didActivateNotification:(NSUserNotification*)notification {
unsigned char bytes[16];
uuidBytesFromNotification(notification, bytes);
uint8_t activationType = 0;
const char* actionValue = NULL;
int64_t actionValueIndex = -1;
switch (notification.activationType) {
case NSUserNotificationActivationTypeActionButtonClicked:
case NSUserNotificationActivationTypeAdditionalActionClicked: {
NSArray* altTitles = [(NSObject*)notification valueForKey:@"_alternateActionButtonTitles"];
if ([altTitles count] > 1) {
NSNumber* altIdx = [(NSObject*)notification valueForKey:@"_alternateActionIndex"];
unsigned long long idx = [altIdx unsignedLongLongValue];
if (idx == (unsigned long long)LONG_MAX) {
actionValue = [notification.actionButtonTitle UTF8String];
} else {
actionValue = [altTitles[idx] UTF8String];
actionValueIndex = (int64_t)idx;
}
} else {
actionValue = [notification.actionButtonTitle UTF8String];
}
activationType = 1;
break;
}
case NSUserNotificationActivationTypeContentsClicked:
activationType = 2;
break;
case NSUserNotificationActivationTypeReplied:
activationType = 3;
actionValue = [notification.response.string UTF8String];
break;
case NSUserNotificationActivationTypeNone:
default:
break;
}
rust_notification_activated(bytes, activationType, actionValue, actionValueIndex);
[center removeDeliveredNotification:notification];
}
- (void)userNotificationCenter:(NSUserNotificationCenter*)center
didDismissAlert:(NSUserNotification*)notification {
unsigned char bytes[16];
uuidBytesFromNotification(notification, bytes);
rust_notification_dismissed(bytes, [notification.otherButtonTitle UTF8String]);
[center removeDeliveredNotification:notification];
}
+ (instancetype)sharedDelegate {
static NotificationCenterDelegate* instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[NotificationCenterDelegate alloc] init];
[NSUserNotificationCenter defaultUserNotificationCenter].delegate = instance;
});
return instance;
}
@end
void setupDelegate(void) {
[NotificationCenterDelegate sharedDelegate];
}