From 303cb4c4288d7b8a3b3f0313a888f2d472ee55fa Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Thu, 22 Feb 2018 15:52:24 +0000 Subject: [PATCH] [notifications] Refactor for better support of separate messages --- ios/RNFirebase/RNFirebaseEvents.h | 2 +- .../messaging/RNFirebaseMessaging.h | 3 +- .../messaging/RNFirebaseMessaging.m | 79 ++++---- .../notifications/RNFirebaseNotifications.m | 153 ++++++++++----- lib/modules/messaging/Message.js | 14 -- lib/modules/messaging/index.js | 7 +- lib/modules/messaging/types.js | 3 - .../notifications/AndroidNotification.js | 180 ++++++------------ lib/modules/notifications/IOSNotification.js | 63 +++--- lib/modules/notifications/Notification.js | 39 ++-- lib/modules/notifications/index.js | 84 ++++---- lib/modules/notifications/types.js | 162 ++++++++++++++++ 12 files changed, 463 insertions(+), 326 deletions(-) create mode 100644 lib/modules/notifications/types.js diff --git a/ios/RNFirebase/RNFirebaseEvents.h b/ios/RNFirebase/RNFirebaseEvents.h index ac40e86d..938a8445 100644 --- a/ios/RNFirebase/RNFirebaseEvents.h +++ b/ios/RNFirebase/RNFirebaseEvents.h @@ -39,8 +39,8 @@ static NSString *const MESSAGING_TOKEN_REFRESHED = @"messaging_token_refreshed"; static NSString *const MESSAGING_NOTIFICATION_RECEIVED = @"messaging_notification_received"; // Notifications -static NSString *const NOTIFICATIONS_NOTIFICATION_CLICKED = @"notifications_notification_clicked"; static NSString *const NOTIFICATIONS_NOTIFICATION_DISPLAYED = @"notifications_notification_displayed"; +static NSString *const NOTIFICATIONS_NOTIFICATION_PRESSED = @"notifications_notification_pressed"; static NSString *const NOTIFICATIONS_NOTIFICATION_RECEIVED = @"notifications_notification_received"; // AdMob diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.h b/ios/RNFirebase/messaging/RNFirebaseMessaging.h index 2aa44131..4db95fa9 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.h +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.h @@ -15,7 +15,8 @@ @property _Nullable RCTPromiseResolveBlock permissionResolver; #if !TARGET_OS_TV -- (void)didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings; +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo; +- (void)didRegisterUserNotificationSettings:(nonnull UIUserNotificationSettings *)notificationSettings; #endif @end diff --git a/ios/RNFirebase/messaging/RNFirebaseMessaging.m b/ios/RNFirebase/messaging/RNFirebaseMessaging.m index 19782e02..8ce69e62 100644 --- a/ios/RNFirebase/messaging/RNFirebaseMessaging.m +++ b/ios/RNFirebase/messaging/RNFirebaseMessaging.m @@ -70,6 +70,12 @@ RCT_EXPORT_MODULE() _permissionResolver = nil; } +// Listen for FCM data messages that arrive as a remote notification +- (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { + NSDictionary *message = [self parseUserInfo:userInfo]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; +} + // ******************************************************* // ** Finish AppDelegate methods // ******************************************************* @@ -88,8 +94,15 @@ RCT_EXPORT_MODULE() // Listen for data messages in the foreground - (void)applicationReceivedRemoteMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { - NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage openedFromTray:false]; + NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage]; + [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; +} +// Receive data messages on iOS 10+ directly from FCM (bypassing APNs) when the app is in the foreground. +// To enable direct data messages, you can set [Messaging messaging].shouldEstablishDirectChannel to YES. +- (void)messaging:(nonnull FIRMessaging *)messaging +didReceiveMessage:(nonnull FIRMessagingRemoteMessage *)remoteMessage { + NSDictionary *message = [self parseFIRMessagingRemoteMessage:remoteMessage]; [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; } @@ -250,8 +263,7 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId // ** Start internals ** -- (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage - openedFromTray:(bool)openedFromTray { +- (NSDictionary*)parseFIRMessagingRemoteMessage:(FIRMessagingRemoteMessage *)remoteMessage { NSDictionary *appData = remoteMessage.appData; NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; @@ -262,46 +274,46 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId } else if ([k1 isEqualToString:@"from"]) { message[@"from"] = appData[k1]; } else if ([k1 isEqualToString:@"notification"]) { - NSDictionary *notification = appData[k1]; - NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; - for (id k2 in notification) { - if ([k2 isEqualToString:@"badge"]) { - notif[@"badge"] = notification[k2]; - } else if ([k2 isEqualToString:@"body"]) { - notif[@"body"] = notification[k2]; - } else if ([k2 isEqualToString:@"body_loc_args"]) { - notif[@"bodyLocalizationArgs"] = notification[k2]; - } else if ([k2 isEqualToString:@"body_loc_key"]) { - notif[@"bodyLocalizationKey"] = notification[k2]; - } else if ([k2 isEqualToString:@"click_action"]) { - notif[@"clickAction"] = notification[k2]; - } else if ([k2 isEqualToString:@"sound"]) { - notif[@"sound"] = notification[k2]; - } else if ([k2 isEqualToString:@"subtitle"]) { - notif[@"subtitle"] = notification[k2]; - } else if ([k2 isEqualToString:@"title"]) { - notif[@"title"] = notification[k2]; - } else if ([k2 isEqualToString:@"title_loc_args"]) { - notif[@"titleLocalizationArgs"] = notification[k2]; - } else if ([k2 isEqualToString:@"title_loc_key"]) { - notif[@"titleLocalizationKey"] = notification[k2]; - } else { - NSLog(@"Unknown notification key: %@", k2); - } - } - message[@"notification"] = notif; + // Ignore for messages } else { // Assume custom data key data[k1] = appData[k1]; } } - message[@"messageType"] = @"RemoteMessage"; message[@"data"] = data; - message[@"openedFromTray"] = @(false); return message; } +- (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo { + NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + + for (id k1 in userInfo) { + if ([k1 isEqualToString:@"aps"]) { + // Ignore notification section + } else if ([k1 isEqualToString:@"gcm.message_id"]) { + message[@"messageId"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"google.c.a.ts"]) { + message[@"sentTime"] = userInfo[k1]; + } else if ([k1 isEqualToString:@"gcm.n.e"] + || [k1 isEqualToString:@"gcm.notification.sound2"] + || [k1 isEqualToString:@"google.c.a.c_id"] + || [k1 isEqualToString:@"google.c.a.c_l"] + || [k1 isEqualToString:@"google.c.a.e"] + || [k1 isEqualToString:@"google.c.a.udt"]) { + // Ignore known keys + } else { + // Assume custom data + data[k1] = userInfo[k1]; + } + } + + message[@"data"] = data; + + return message; +} + - (NSArray *)supportedEvents { return @[MESSAGING_MESSAGE_RECEIVED, MESSAGING_TOKEN_REFRESHED]; } @@ -317,4 +329,3 @@ RCT_EXPORT_METHOD(completeRemoteNotification: (NSString*) messageId @implementation RNFirebaseMessaging @end #endif - diff --git a/ios/RNFirebase/notifications/RNFirebaseNotifications.m b/ios/RNFirebase/notifications/RNFirebaseNotifications.m index abdcd338..fb602149 100644 --- a/ios/RNFirebase/notifications/RNFirebaseNotifications.m +++ b/ios/RNFirebase/notifications/RNFirebaseNotifications.m @@ -2,6 +2,7 @@ #if __has_include() #import "RNFirebaseEvents.h" +#import "RNFirebaseMessaging.h" #import "RNFirebaseUtil.h" #import @@ -37,7 +38,7 @@ RCT_EXPORT_MODULE(); - (void)configure { // If we're on iOS 10 then we need to set this as a delegate for the UNUserNotificationCenter #if defined(__IPHONE_10_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0 - [UNUserNotificationCenter currentNotificationCenter].delegate = self; + // [UNUserNotificationCenter currentNotificationCenter].delegate = self; #endif // Set static instance for use from AppDelegate @@ -50,26 +51,78 @@ RCT_EXPORT_MODULE(); // ******************************************************* - (void)didReceiveLocalNotification:(nonnull UILocalNotification *)notification { - + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + + NSDictionary *message = [self parseUILocalNotification:notification]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // Listen for background messages - (void)didReceiveRemoteNotification:(nonnull NSDictionary *)userInfo { - BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil openedFromTray:isFromBackground]; + // FCM Data messages come through here if they specify content-available=true + // Pass them over to the RNFirebaseMessaging handler instead + if (userInfo[@"aps"] && ((NSDictionary*)userInfo[@"aps"]).count == 1 && userInfo[@"aps"][@"content-available"]) { + [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; + return; + } - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + + // TODO: Proper notification structure + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotification" clickAction:nil]; + + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // Listen for background messages - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { - BOOL isFromBackground = (RCTSharedApplication().applicationState == UIApplicationStateInactive); - NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" clickAction:nil openedFromTray:isFromBackground]; + // FCM Data messages come through here if they specify content-available=true + // Pass them over to the RNFirebaseMessaging handler instead + if (userInfo[@"aps"] && ((NSDictionary*)userInfo[@"aps"]).count == 1 && userInfo[@"aps"][@"content-available"]) { + [[RNFirebaseMessaging instance] didReceiveRemoteNotification:userInfo]; + return; + } + NSString *event; + if (RCTSharedApplication().applicationState == UIApplicationStateBackground) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; + } else if (RCTSharedApplication().applicationState == UIApplicationStateInactive) { + // notification_displayed + event = NOTIFICATIONS_NOTIFICATION_PRESSED; + } else { + // notification_received + event = NOTIFICATIONS_NOTIFICATION_RECEIVED; + } + + // TODO: Proper notification structure + NSDictionary *message = [self parseUserInfo:userInfo messageType:@"RemoteNotificationHandler" clickAction:nil]; + + // TODO: Callback handler // [_callbackHandlers setObject:[completionHandler copy] forKey:message[@"messageId"]]; - [RNFirebaseUtil sendJSEvent:self name:MESSAGING_MESSAGE_RECEIVED body:message]; + [RNFirebaseUtil sendJSEvent:self name:event body:message]; } // ******************************************************* @@ -93,7 +146,7 @@ RCT_EXPORT_MODULE(); NSString *event; UNNotificationPresentationOptions options; - NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification" openedFromTray:false]; + NSDictionary *message = [self parseUNNotification:notification messageType:@"PresentNotification"]; if (isFcm || isScheduled) { // If app is in the background @@ -116,9 +169,7 @@ RCT_EXPORT_MODULE(); event = NOTIFICATIONS_NOTIFICATION_DISPLAYED; } - if (event) { - [RNFirebaseUtil sendJSEvent:self name:event body:message]; - } + [RNFirebaseUtil sendJSEvent:self name:event body:message]; completionHandler(options); } @@ -130,9 +181,9 @@ didReceiveNotificationResponse:(UNNotificationResponse *)response #else withCompletionHandler:(void(^)())completionHandler { #endif - NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse" openedFromTray:true]; + NSDictionary *message = [self parseUNNotification:response.notification messageType:@"NotificationResponse"]; - [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_CLICKED body:message]; + [RNFirebaseUtil sendJSEvent:self name:NOTIFICATIONS_NOTIFICATION_PRESSED body:message]; completionHandler(); } @@ -200,7 +251,13 @@ RCT_EXPORT_METHOD(getInitialNotification:(RCTPromiseResolveBlock)resolve rejecte NSDictionary *notification = [self parseUILocalNotification:localNotification]; resolve(notification); } else { - resolve(nil); + NSDictionary *remoteNotification = [self bridge].launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; + if (remoteNotification) { + NSDictionary *message = [self parseUserInfo:remoteNotification messageType:@"InitialMessage" clickAction:nil]; + resolve(message); + } else { + resolve(nil); + } } } @@ -485,7 +542,7 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } ios[@"attachments"] = attachments; } - + if (localNotification.content.badge) { ios[@"badge"] = localNotification.content.badge; } @@ -504,21 +561,20 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } - (NSDictionary*)parseUNNotification:(UNNotification *)notification - messageType:(NSString *)messageType - openedFromTray:(bool)openedFromTray { + messageType:(NSString *)messageType { NSDictionary *userInfo = notification.request.content.userInfo; NSString *clickAction = notification.request.content.categoryIdentifier; - return [self parseUserInfo:userInfo messageType:messageType clickAction:clickAction openedFromTray:openedFromTray]; + return [self parseUserInfo:userInfo messageType:messageType clickAction:clickAction]; } - (NSDictionary*)parseUserInfo:(NSDictionary *)userInfo messageType:(NSString *) messageType - clickAction:(NSString *) clickAction - openedFromTray:(bool)openedFromTray { - NSMutableDictionary *message = [[NSMutableDictionary alloc] init]; - NSMutableDictionary *notif = [[NSMutableDictionary alloc] init]; + clickAction:(NSString *) clickAction { + + NSMutableDictionary *notification = [[NSMutableDictionary alloc] init]; NSMutableDictionary *data = [[NSMutableDictionary alloc] init]; + NSMutableDictionary *ios = [[NSMutableDictionary alloc] init]; for (id k1 in userInfo) { if ([k1 isEqualToString:@"aps"]) { @@ -528,37 +584,42 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification NSDictionary *alert = aps[k2]; for (id k3 in alert) { if ([k3 isEqualToString:@"body"]) { - notif[@"body"] = alert[k3]; + notification[@"body"] = alert[k3]; } else if ([k3 isEqualToString:@"loc-args"]) { - notif[@"bodyLocalizationArgs"] = alert[k3]; + // TODO: What to do with this? + // notif[@"bodyLocalizationArgs"] = alert[k3]; } else if ([k3 isEqualToString:@"loc-key"]) { - notif[@"bodyLocalizationKey"] = alert[k3]; + // TODO: What to do with this? + // notif[@"bodyLocalizationKey"] = alert[k3]; } else if ([k3 isEqualToString:@"subtitle"]) { - notif[@"subtitle"] = alert[k3]; + notification[@"subtitle"] = alert[k3]; } else if ([k3 isEqualToString:@"title"]) { - notif[@"title"] = alert[k3]; + notification[@"title"] = alert[k3]; } else if ([k3 isEqualToString:@"title-loc-args"]) { - notif[@"titleLocalizationArgs"] = alert[k3]; + // TODO: What to do with this? + // notif[@"titleLocalizationArgs"] = alert[k3]; } else if ([k3 isEqualToString:@"title-loc-key"]) { - notif[@"titleLocalizationKey"] = alert[k3]; + // TODO: What to do with this? + // notif[@"titleLocalizationKey"] = alert[k3]; } else { NSLog(@"Unknown alert key: %@", k2); } } } else if ([k2 isEqualToString:@"badge"]) { - notif[@"badge"] = aps[k2]; + ios[@"badge"] = aps[k2]; } else if ([k2 isEqualToString:@"category"]) { - notif[@"clickAction"] = aps[k2]; + ios[@"category"] = aps[k2]; } else if ([k2 isEqualToString:@"sound"]) { - notif[@"sound"] = aps[k2]; + notification[@"sound"] = aps[k2]; } else { NSLog(@"Unknown aps key: %@", k2); } } } else if ([k1 isEqualToString:@"gcm.message_id"]) { - message[@"messageId"] = userInfo[k1]; + notification[@"notificationId"] = userInfo[k1]; } else if ([k1 isEqualToString:@"google.c.a.ts"]) { - message[@"sentTime"] = userInfo[k1]; + // TODO: What to do with this? + // message[@"sentTime"] = userInfo[k1]; } else if ([k1 isEqualToString:@"gcm.n.e"] || [k1 isEqualToString:@"gcm.notification.sound2"] || [k1 isEqualToString:@"google.c.a.c_id"] @@ -572,26 +633,17 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification } } - if (!notif[@"clickAction"] && clickAction) { - notif[@"clickAction"] = clickAction; - } + // TODO: What to do with this? + // message[@"messageType"] = messageType; - // Generate a message ID if one was not present in the notification - // This is used for resolving click handlers - if (!message[@"messageId"]) { - message[@"messageId"] = [[NSUUID UUID] UUIDString]; - } - message[@"messageType"] = messageType; + notification[@"data"] = data; + notification[@"ios"] = ios; - message[@"data"] = data; - message[@"notification"] = notif; - message[@"openedFromTray"] = @(openedFromTray); - - return message; + return notification; } - (NSArray *)supportedEvents { - return @[NOTIFICATIONS_NOTIFICATION_CLICKED, NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; + return @[NOTIFICATIONS_NOTIFICATION_DISPLAYED, NOTIFICATIONS_NOTIFICATION_PRESSED, NOTIFICATIONS_NOTIFICATION_RECEIVED]; } + (BOOL)requiresMainQueueSetup @@ -605,4 +657,3 @@ RCT_EXPORT_METHOD(scheduleNotification:(NSDictionary*) notification @implementation RNFirebaseNotifications @end #endif - diff --git a/lib/modules/messaging/Message.js b/lib/modules/messaging/Message.js index d789377e..38f3433f 100644 --- a/lib/modules/messaging/Message.js +++ b/lib/modules/messaging/Message.js @@ -11,9 +11,7 @@ import { } from './types'; import type Messaging from './'; import type { - MessageTypeType, NativeMessage, - Notification, PresentNotificationResultType, RemoteNotificationResultType, } from './types'; @@ -47,18 +45,6 @@ export default class Message { return this._message.messageId; } - get messageType(): ?MessageTypeType { - return this._message.messageType; - } - - get openedFromTray(): boolean { - return this._message.openedFromTray; - } - - get notification(): ?Notification { - return this._message.notification; - } - get sentTime(): ?number { return this._message.sentTime; } diff --git a/lib/modules/messaging/index.js b/lib/modules/messaging/index.js index b4b2abc8..f17d389a 100644 --- a/lib/modules/messaging/index.js +++ b/lib/modules/messaging/index.js @@ -55,8 +55,8 @@ export default class Messaging extends ModuleBase { // sub to internal native event - this fans out to // public event name: onMessage 'messaging_message_received', - (message: Message) => { - SharedEventEmitter.emit('onMessage', message); + (message: NativeMessage) => { + SharedEventEmitter.emit('onMessage', new Message(this, message)); } ); @@ -89,8 +89,7 @@ export default class Messaging extends ModuleBase { getLogger(this).info('Creating onMessage listener'); - const wrappedListener = async (nativeMessage: NativeMessage) => { - const message = new Message(this, nativeMessage); + const wrappedListener = async (message: Message) => { await listener(message); message.complete(); }; diff --git a/lib/modules/messaging/types.js b/lib/modules/messaging/types.js index 1909660b..3b58201a 100644 --- a/lib/modules/messaging/types.js +++ b/lib/modules/messaging/types.js @@ -51,9 +51,6 @@ export type NativeMessage = { data: { [string]: string }, from?: string, messageId: string, - messageType?: MessageTypeType, - openedFromTray: boolean, - notification?: Notification, sentTime?: number, to?: string, ttl?: number, diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index f2591707..c3305e0b 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -2,128 +2,29 @@ * @flow * AndroidNotification representation wrapper */ +import { Category } from './types'; import type Notification from './Notification'; - -type Lights = { - argb: number, - onMs: number, - offMs: number, -}; - -type Progress = { - max: number, - progress: number, - indeterminate: boolean, -}; - -type SmallIcon = { - icon: string, - level?: number, -}; - -export type NativeAndroidNotification = {| - // TODO actions: Action[], - autoCancel: boolean, - badgeIconType: BadgeIconTypeType, - category: CategoryType, - channelId: string, - clickAction?: string, - color: string, - colorized: boolean, - contentInfo: string, - defaults: DefaultsType[], - group: string, - groupAlertBehaviour: GroupAlertType, - groupSummary: boolean, - largeIcon: string, - lights: Lights, - localOnly: boolean, - number: number, - ongoing: boolean, - onlyAlertOnce: boolean, - people: string[], - priority: PriorityType, - progress: Progress, - // publicVersion: Notification, - remoteInputHistory: string[], - shortcutId: string, - showWhen: boolean, - smallIcon: SmallIcon, - sortKey: string, - // TODO: style: Style, - ticker: string, - timeoutAfter: number, - usesChronometer: boolean, - vibrate: number[], - visibility: VisibilityType, - when: number, -|}; - -export const BadgeIconType = { - Large: 2, - None: 0, - Small: 1, -}; - -export const Category = { - Alarm: 'alarm', - Call: 'call', - Email: 'email', - Error: 'err', - Event: 'event', - Message: 'msg', - Progress: 'progress', - Promo: 'promo', - Recommendation: 'recommendation', - Reminder: 'reminder', - Service: 'service', - Social: 'social', - Status: 'status', - System: 'system', - Transport: 'transport', -}; - -export const Defaults = { - All: -1, - Lights: 4, - Sound: 1, - Vibrate: 2, -}; - -export const GroupAlert = { - All: 0, - Children: 2, - Summary: 1, -}; - -export const Priority = { - Default: 0, - High: 1, - Low: -1, - Max: 2, - Min: -2, -}; - -export const Visibility = { - Private: 0, - Public: 1, - Secret: -1, -}; - -type BadgeIconTypeType = $Values; -type CategoryType = $Values; -type DefaultsType = $Values; -type GroupAlertType = $Values; -type PriorityType = $Values; -type VisibilityType = $Values; +import type { + BadgeIconTypeType, + CategoryType, + DefaultsType, + GroupAlertType, + Lights, + NativeAndroidNotification, + PriorityType, + Progress, + SmallIcon, + VisibilityType, +} from './types'; export default class AndroidNotification { + // TODO optional fields // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) _autoCancel: boolean; _badgeIconType: BadgeIconTypeType; _category: CategoryType; _channelId: string; - _clickAction: string; + _clickAction: string | void; _color: string; _colorized: boolean; _contentInfo: string; @@ -145,9 +46,7 @@ export default class AndroidNotification { _remoteInputHistory: string[]; _shortcutId: string; _showWhen: boolean; - _smallIcon: SmallIcon = { - icon: 'ic_launcher', - }; + _smallIcon: SmallIcon; _sortKey: string; // TODO: style: Style; // Need to figure out if this can work _ticker: string; @@ -167,9 +66,50 @@ export default class AndroidNotification { // fullScreenIntent: PendingIntent // sound.streamType - constructor(notification: Notification) { + constructor(notification: Notification, data?: NativeAndroidNotification) { this._notification = notification; - this._people = []; + + if (data) { + this._autoCancel = data.autoCancel; + this._badgeIconType = data.badgeIconType; + this._category = data.category; + this._channelId = data.channelId; + this._clickAction = data.clickAction; + this._color = data.color; + this._colorized = data.colorized; + this._contentInfo = data.contentInfo; + this._defaults = data.defaults; + this._group = data.group; + this._groupAlertBehaviour = data.groupAlertBehaviour; + this._groupSummary = data.groupSummary; + this._largeIcon = data.largeIcon; + this._lights = data.lights; + this._localOnly = data.localOnly; + this._number = data.number; + this._ongoing = data.ongoing; + this._onlyAlertOnce = data.onlyAlertOnce; + this._people = data.people; + this._priority = data.priority; + this._progress = data.progress; + // _publicVersion: Notification; + this._remoteInputHistory = data.remoteInputHistory; + this._shortcutId = data.shortcutId; + this._showWhen = data.showWhen; + this._smallIcon = data.smallIcon; + this._sortKey = data.sortKey; + this._ticker = data.ticker; + this._timeoutAfter = data.timeoutAfter; + this._usesChronometer = data.usesChronometer; + this._vibrate = data.vibrate; + this._visibility = data.visibility; + this._when = data.when; + } + + // Defaults + this._people = this._people || []; + this._smallIcon = this._smallIcon || { + icon: 'ic_launcher', + }; } /** @@ -506,7 +446,7 @@ export default class AndroidNotification { } build(): NativeAndroidNotification { - // TODO: Validation + // TODO: Validation of required fields if (!this._channelId) { throw new Error( 'AndroidNotification: Missing required `channelId` property' diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js index d4ad7e11..5b1e563b 100644 --- a/lib/modules/notifications/IOSNotification.js +++ b/lib/modules/notifications/IOSNotification.js @@ -3,48 +3,37 @@ * IOSNotification representation wrapper */ import type Notification from './Notification'; - -type AttachmentOptions = {| - TypeHint: string, - ThumbnailHidden: boolean, - ThumbnailClippingRect: { - height: number, - width: number, - x: number, - y: number, - }, - ThumbnailTime: number, -|}; - -type Attachment = {| - identifier: string, - options?: AttachmentOptions, - url: string, -|}; - -export type NativeIOSNotification = {| - alertAction?: string, - attachments: Attachment[], - badge?: number, - category?: string, - hasAction?: boolean, - launchImage?: string, - threadIdentifier?: string, -|}; +import type { + Attachment, + AttachmentOptions, + NativeIOSNotification, +} from './types'; export default class IOSNotification { - _alertAction: string; // alertAction | N/A + _alertAction: string | void; // alertAction | N/A _attachments: Attachment[]; // N/A | attachments - _badge: number; // applicationIconBadgeNumber | badge - _category: string; - _hasAction: boolean; // hasAction | N/A - _launchImage: string; // alertLaunchImage | launchImageName + _badge: number | void; // applicationIconBadgeNumber | badge + _category: string | void; + _hasAction: boolean | void; // hasAction | N/A + _launchImage: string | void; // alertLaunchImage | launchImageName _notification: Notification; - _threadIdentifier: string; // N/A | threadIdentifier + _threadIdentifier: string | void; // N/A | threadIdentifier - constructor(notification: Notification) { - this._attachments = []; + constructor(notification: Notification, data?: NativeIOSNotification) { this._notification = notification; + + if (data) { + this._alertAction = data.alertAction; + this._attachments = data.attachments; + this._badge = data.badge; + this._category = data.category; + this._hasAction = data.hasAction; + this._launchImage = data.launchImage; + this._threadIdentifier = data.threadIdentifier; + } + + // Defaults + this._attachments = this._attachments || []; } /** @@ -128,7 +117,7 @@ export default class IOSNotification { } build(): NativeIOSNotification { - // TODO: Validation + // TODO: Validation of required fields return { alertAction: this._alertAction, diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index bcaae87b..50ec6b5d 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -7,21 +7,7 @@ import AndroidNotification from './AndroidNotification'; import IOSNotification from './IOSNotification'; import { generatePushID, isObject } from '../../utils'; -import type { NativeAndroidNotification } from './AndroidNotification'; -import type { NativeIOSNotification } from './IOSNotification'; -import type { Schedule } from './'; - -type NativeNotification = {| - android?: NativeAndroidNotification, - body: string, - data: { [string]: string }, - ios?: NativeIOSNotification, - notificationId: string, - schedule?: Schedule, - sound?: string, - subtitle?: string, - title: string, -|}; +import type { NativeNotification } from './types'; export default class Notification { // iOS 8/9 | 10+ | Android @@ -34,12 +20,23 @@ export default class Notification { _subtitle: string | void; // N/A | subtitle | subText _title: string; // alertTitle | title | contentTitle - constructor() { - this._android = new AndroidNotification(this); - this._data = {}; - this._ios = new IOSNotification(this); - // TODO: Is this the best way to generate an ID? - this._notificationId = generatePushID(); + constructor(data?: NativeNotification) { + this._android = new AndroidNotification(this, data && data.android); + this._ios = new IOSNotification(this, data && data.ios); + + if (data) { + this._body = data.body; + this._data = data.data; + // TODO: Is this the best way to generate an ID? + this._notificationId = data.notificationId; + this._sound = data.sound; + this._subtitle = data.subtitle; + this._title = data.title; + } + + // Defaults + this._data = this._data || {}; + this._notificationId = this._notificationId || generatePushID(); } get android(): AndroidNotification { diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 57aed391..6b9f3e24 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -15,26 +15,20 @@ import { GroupAlert, Priority, Visibility, -} from './AndroidNotification'; +} from './types'; import type App from '../core/app'; +import type { NativeNotification, Schedule } from './types'; -// TODO: Received notification type will be different from sent notification type OnNotification = Notification => any; type OnNotificationObserver = { next: OnNotification, }; -export type Schedule = { - exact?: boolean, - fireDate: number, - repeatInterval?: 'minute' | 'hour' | 'day' | 'week', -}; - const NATIVE_EVENTS = [ - 'notifications_notification_clicked', 'notifications_notification_displayed', + 'notifications_notification_pressed', 'notifications_notification_received', ]; @@ -69,10 +63,13 @@ export default class Notifications extends ModuleBase { SharedEventEmitter.addListener( // sub to internal native event - this fans out to - // public event name: onNotificationClicked - 'notifications_notification_clicked', - (notification: Notification) => { - SharedEventEmitter.emit('onNotificationClicked', notification); + // public event name: onNotificationPressed + 'notifications_notification_pressed', + (notification: NativeNotification) => { + SharedEventEmitter.emit( + 'onNotificationPressed', + new Notification(notification) + ); } ); @@ -80,8 +77,11 @@ export default class Notifications extends ModuleBase { // sub to internal native event - this fans out to // public event name: onNotificationDisplayed 'notifications_notification_displayed', - (notification: Notification) => { - SharedEventEmitter.emit('onNotificationDisplayed', notification); + (notification: NativeNotification) => { + SharedEventEmitter.emit( + 'onNotificationDisplayed', + new Notification(notification) + ); } ); @@ -89,8 +89,11 @@ export default class Notifications extends ModuleBase { // sub to internal native event - this fans out to // public event name: onNotification 'notifications_notification_received', - (notification: Notification) => { - SharedEventEmitter.emit('onNotification', notification); + (notification: NativeNotification) => { + SharedEventEmitter.emit( + 'onNotification', + new Notification(notification) + ); } ); } @@ -166,29 +169,6 @@ export default class Notifications extends ModuleBase { }; } - onNotificationClicked( - nextOrObserver: OnNotification | OnNotificationObserver - ): () => any { - let listener; - if (isFunction(nextOrObserver)) { - listener = nextOrObserver; - } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { - listener = nextOrObserver.next; - } else { - throw new Error( - 'Notifications.onNotificationClicked failed: First argument must be a function or observer object with a `next` function.' - ); - } - - getLogger(this).info('Creating onNotificationClicked listener'); - SharedEventEmitter.addListener('onNotificationClicked', listener); - - return () => { - getLogger(this).info('Removing onNotificationClicked listener'); - SharedEventEmitter.removeListener('onNotificationClicked', listener); - }; - } - onNotificationDisplayed( nextOrObserver: OnNotification | OnNotificationObserver ): () => any { @@ -212,6 +192,30 @@ export default class Notifications extends ModuleBase { }; } + onNotificationPressed( + nextOrObserver: OnNotification | OnNotificationObserver + ): () => any { + let listener: Notification => any; + if (isFunction(nextOrObserver)) { + // $FlowBug: Not coping with the overloaded method signature + listener = nextOrObserver; + } else if (isObject(nextOrObserver) && isFunction(nextOrObserver.next)) { + listener = nextOrObserver.next; + } else { + throw new Error( + 'Notifications.onNotificationPressed failed: First argument must be a function or observer object with a `next` function.' + ); + } + + getLogger(this).info('Creating onNotificationPressed listener'); + SharedEventEmitter.addListener('onNotificationPressed', listener); + + return () => { + getLogger(this).info('Removing onNotificationPressed listener'); + SharedEventEmitter.removeListener('onNotificationPressed', listener); + }; + } + /** * Remove all delivered notifications. * @returns {*} diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js new file mode 100644 index 00000000..eec87988 --- /dev/null +++ b/lib/modules/notifications/types.js @@ -0,0 +1,162 @@ +/** + * @flow + */ + +export const BadgeIconType = { + Large: 2, + None: 0, + Small: 1, +}; + +export const Category = { + Alarm: 'alarm', + Call: 'call', + Email: 'email', + Error: 'err', + Event: 'event', + Message: 'msg', + Progress: 'progress', + Promo: 'promo', + Recommendation: 'recommendation', + Reminder: 'reminder', + Service: 'service', + Social: 'social', + Status: 'status', + System: 'system', + Transport: 'transport', +}; + +export const Defaults = { + All: -1, + Lights: 4, + Sound: 1, + Vibrate: 2, +}; + +export const GroupAlert = { + All: 0, + Children: 2, + Summary: 1, +}; + +export const Priority = { + Default: 0, + High: 1, + Low: -1, + Max: 2, + Min: -2, +}; + +export const Visibility = { + Private: 0, + Public: 1, + Secret: -1, +}; + +export type BadgeIconTypeType = $Values; +export type CategoryType = $Values; +export type DefaultsType = $Values; +export type GroupAlertType = $Values; +export type PriorityType = $Values; +export type VisibilityType = $Values; + +export type Lights = { + argb: number, + onMs: number, + offMs: number, +}; + +export type Progress = { + max: number, + progress: number, + indeterminate: boolean, +}; + +export type SmallIcon = { + icon: string, + level?: number, +}; + +export type NativeAndroidNotification = {| + // TODO actions: Action[], + autoCancel: boolean, + badgeIconType: BadgeIconTypeType, + category: CategoryType, + channelId: string, + clickAction?: string, + color: string, + colorized: boolean, + contentInfo: string, + defaults: DefaultsType[], + group: string, + groupAlertBehaviour: GroupAlertType, + groupSummary: boolean, + largeIcon: string, + lights: Lights, + localOnly: boolean, + number: number, + ongoing: boolean, + onlyAlertOnce: boolean, + people: string[], + priority: PriorityType, + progress: Progress, + // publicVersion: Notification, + remoteInputHistory: string[], + shortcutId: string, + showWhen: boolean, + smallIcon: SmallIcon, + sortKey: string, + // TODO: style: Style, + ticker: string, + timeoutAfter: number, + usesChronometer: boolean, + vibrate: number[], + visibility: VisibilityType, + when: number, +|}; + +export type AttachmentOptions = {| + TypeHint: string, + ThumbnailHidden: boolean, + ThumbnailClippingRect: { + height: number, + width: number, + x: number, + y: number, + }, + ThumbnailTime: number, +|}; + +export type Attachment = {| + identifier: string, + options?: AttachmentOptions, + url: string, +|}; + +export type NativeIOSNotification = {| + alertAction?: string, + attachments: Attachment[], + badge?: number, + category?: string, + hasAction?: boolean, + launchImage?: string, + threadIdentifier?: string, +|}; + +export type Schedule = { + exact?: boolean, + fireDate: number, + repeatInterval?: 'minute' | 'hour' | 'day' | 'week', +}; + +export type NativeNotification = {| + android?: NativeAndroidNotification, + body: string, + data: { [string]: string }, + ios?: NativeIOSNotification, + notificationId: string, + schedule?: Schedule, + sound?: string, + subtitle?: string, + title: string, +|};