From e537955212c17126ac235ee612f2b7436f92e8d8 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Mar 2018 11:09:20 +0000 Subject: [PATCH] [notifications] Add support for android actions --- .../messaging/BundleJSONConverter.java | 38 +++-- .../RNFirebaseNotificationManager.java | 107 +++++++++++-- .../RNFirebaseNotifications.java | 8 +- lib/modules/notifications/AndroidAction.js | 150 ++++++++++++++++++ lib/modules/notifications/AndroidChannel.js | 45 ++---- .../notifications/AndroidChannelGroup.js | 27 +--- .../notifications/AndroidNotification.js | 50 +++++- .../notifications/AndroidRemoteInput.js | 123 ++++++++++++++ lib/modules/notifications/Notification.js | 5 +- lib/modules/notifications/index.js | 8 + lib/modules/notifications/types.js | 56 +++++-- tests/src/firebase.js | 25 ++- 12 files changed, 539 insertions(+), 103 deletions(-) create mode 100644 lib/modules/notifications/AndroidAction.js create mode 100644 lib/modules/notifications/AndroidRemoteInput.js diff --git a/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java b/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java index 4e7446d3..d784f694 100644 --- a/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java +++ b/android/src/main/java/io/invertase/firebase/messaging/BundleJSONConverter.java @@ -110,23 +110,28 @@ public class BundleJSONConverter { SETTERS.put(JSONArray.class, new Setter() { public void setOnBundle(Bundle bundle, String key, Object value) throws JSONException { JSONArray jsonArray = (JSONArray) value; - ArrayList stringArrayList = new ArrayList(); // Empty list, can't even figure out the type, assume an ArrayList if (jsonArray.length() == 0) { - bundle.putStringArrayList(key, stringArrayList); + bundle.putStringArrayList(key, new ArrayList()); return; } // Only strings are supported for now - for (int i = 0; i < jsonArray.length(); i++) { - Object current = jsonArray.get(i); - if (current instanceof String) { - stringArrayList.add((String) current); - } else { - throw new IllegalArgumentException("Unexpected type in an array: " + current.getClass()); + if (jsonArray.get(0) instanceof String) { + ArrayList stringArrayList = new ArrayList(); + for (int i = 0; i < jsonArray.length(); i++) { + stringArrayList.add((String) jsonArray.get(i)); } + bundle.putStringArrayList(key, stringArrayList); + } else if (jsonArray.get(0) instanceof JSONObject) { + ArrayList bundleArrayList = new ArrayList<>(); + for (int i =0; i < jsonArray.length(); i++) { + bundleArrayList.add(convertToBundle((JSONObject) jsonArray.get(i))); + } + bundle.putSerializable(key, bundleArrayList); + } else { + throw new IllegalArgumentException("Unexpected type in an array: " + jsonArray.get(0).getClass()); } - bundle.putStringArrayList(key, stringArrayList); } @Override @@ -152,13 +157,18 @@ public class BundleJSONConverter { continue; } - // Special case List as getClass would not work, since List is an interface + // Special case List as getClass would not work, since List is an interface if (value instanceof List) { JSONArray jsonArray = new JSONArray(); - @SuppressWarnings("unchecked") - List listValue = (List) value; - for (String stringValue : listValue) { - jsonArray.put(stringValue); + List listValue = (List) value; + for (Object objValue : listValue) { + if (objValue instanceof String) { + jsonArray.put(objValue); + } else if (objValue instanceof Bundle) { + jsonArray.put(convertToJSON((Bundle) objValue)); + } else { + throw new IllegalArgumentException("Unsupported type: " + objValue.getClass()); + } } json.put(key, jsonArray); continue; diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java index 310f63a9..3d8c3923 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationManager.java @@ -17,7 +17,9 @@ import android.media.RingtoneManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.Parcelable; import android.support.v4.app.NotificationCompat; +import android.support.v4.app.RemoteInput; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; @@ -325,10 +327,8 @@ public class RNFirebaseNotificationManager { } if (android.containsKey("smallIcon")) { Bundle smallIcon = android.getBundle("smallIcon"); - int smallIconResourceId = getResourceId("mipmap", smallIcon.getString("icon")); - if (smallIconResourceId == 0) { - smallIconResourceId = getResourceId("drawable", smallIcon.getString("icon")); - } + int smallIconResourceId = getIcon(smallIcon.getString("icon")); + if (smallIconResourceId != 0) { if (smallIcon.containsKey("level")) { Double level = smallIcon.getDouble("level"); @@ -386,17 +386,17 @@ public class RNFirebaseNotificationManager { notification.setStyle(bigPicture); } */ - - // Create the notification intent - Intent intent = new Intent(context, intentClass); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - intent.putExtras(notification); - if (android.containsKey("clickAction")) { - intent.setAction(android.getString("clickAction")); + // Build any actions + if (android.containsKey("actions")) { + List actions = (List) android.getSerializable("actions"); + for (Bundle a : actions) { + NotificationCompat.Action action = createAction(a, intentClass, notification); + nb = nb.addAction(action); + } } - PendingIntent contentIntent = PendingIntent.getActivity(context, notificationId.hashCode(), intent, - PendingIntent.FLAG_UPDATE_CURRENT); + // Create the notification intent + PendingIntent contentIntent = createIntent(intentClass, notification, android.getString("clickAction")); nb = nb.setContentIntent(contentIntent); // Build the notification and send it @@ -415,6 +415,75 @@ public class RNFirebaseNotificationManager { } } + private NotificationCompat.Action createAction(Bundle action, Class intentClass, Bundle notification) { + String actionKey = action.getString("action"); + PendingIntent actionIntent = createIntent(intentClass, notification, actionKey); + + int icon = getIcon(action.getString("icon")); + String title = action.getString("title"); + + NotificationCompat.Action.Builder ab = new NotificationCompat.Action.Builder(icon, title, actionIntent); + + if (action.containsKey("allowGeneratedReplies")) { + ab = ab.setAllowGeneratedReplies(action.getBoolean("allowGeneratedReplies")); + } + if (action.containsKey("remoteInputs")) { + List remoteInputs = (List) action.getSerializable("remoteInputs"); + for (Bundle ri : remoteInputs) { + RemoteInput remoteInput = createRemoteInput(ri); + ab = ab.addRemoteInput(remoteInput); + } + } + // TODO: SemanticAction and ShowsUserInterface only available on v28? + // if (action.containsKey("semanticAction")) { + // Double semanticAction = action.getDouble("semanticAction"); + // ab = ab.setSemanticAction(semanticAction.intValue()); + // } + // if (action.containsKey("showsUserInterface")) { + // ab = ab.setShowsUserInterface(action.getBoolean("showsUserInterface")); + // } + + return ab.build(); + } + + private PendingIntent createIntent(Class intentClass, Bundle notification, String action) { + Intent intent = new Intent(context, intentClass); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.putExtras(notification); + + if (action != null) { + intent.setAction(action); + } + + String notificationId = notification.getString("notificationId"); + + return PendingIntent.getActivity(context, notificationId.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private RemoteInput createRemoteInput(Bundle remoteInput) { + String resultKey = remoteInput.getString("resultKey"); + + RemoteInput.Builder rb = new RemoteInput.Builder(resultKey); + + if (remoteInput.containsKey("allowedDataTypes")) { + List allowedDataTypes = (List) remoteInput.getSerializable("allowedDataTypes"); + for (Bundle adt : allowedDataTypes) { + rb.setAllowDataType(adt.getString("mimeType"), adt.getBoolean("allow")); + } + } + if (remoteInput.containsKey("allowFreeFormInput")) { + rb.setAllowFreeFormInput(remoteInput.getBoolean("allowFreeFormInput")); + } + if (remoteInput.containsKey("choices")) { + rb.setChoices(remoteInput.getStringArray("choices")); + } + if (remoteInput.containsKey("label")) { + rb.setLabel(remoteInput.getString("label")); + } + + return rb.build(); + } + private Bitmap getBitmap(String image) { if (image.startsWith("http://") || image.startsWith("https://")) { return getBitmapFromUrl(image); @@ -436,6 +505,14 @@ public class RNFirebaseNotificationManager { } } + private int getIcon(String icon) { + int smallIconResourceId = getResourceId("mipmap", icon); + if (smallIconResourceId == 0) { + smallIconResourceId = getResourceId("drawable", icon); + } + return smallIconResourceId; + } + private Class getMainActivityClass() { String packageName = context.getPackageName(); Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); @@ -555,9 +632,9 @@ public class RNFirebaseNotificationManager { PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationId.hashCode(), notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); - if (schedule.containsKey("interval")) { + if (schedule.containsKey("repeatInterval")) { Long interval = null; - switch (schedule.getString("interval")) { + switch (schedule.getString("repeatInterval")) { case "minute": interval = 60000L; break; diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java index 879be98a..65c487d5 100644 --- a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -7,12 +7,12 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; +import android.support.v4.app.RemoteInput; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.bridge.ActivityEventListener; import com.facebook.react.bridge.Arguments; -import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -198,6 +198,12 @@ public class RNFirebaseNotifications extends ReactContextBaseJavaModule implemen notificationOpenMap.putString("action", intent.getAction()); notificationOpenMap.putMap("notification", notificationMap); + // Check for remote input results + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + notificationOpenMap.putMap("results", Arguments.makeNativeMap(remoteInput)); + } + return notificationOpenMap; } diff --git a/lib/modules/notifications/AndroidAction.js b/lib/modules/notifications/AndroidAction.js new file mode 100644 index 00000000..ed2a940c --- /dev/null +++ b/lib/modules/notifications/AndroidAction.js @@ -0,0 +1,150 @@ +/** + * @flow + * AndroidAction representation wrapper + */ +import RemoteInput, { + fromNativeAndroidRemoteInput, +} from './AndroidRemoteInput'; +import { SemanticAction } from './types'; +import type { NativeAndroidAction, SemanticActionType } from './types'; + +export default class AndroidAction { + _action: string; + _allowGeneratedReplies: boolean | void; + _icon: string; + _remoteInputs: RemoteInput[]; + _semanticAction: SemanticActionType | void; + _showUserInterface: boolean | void; + _title: string; + + constructor(action: string, icon: string, title: string) { + this._action = action; + this._icon = icon; + this._remoteInputs = []; + this._title = title; + } + + get action(): string { + return this._action; + } + + get allowGeneratedReplies(): ?boolean { + return this._allowGeneratedReplies; + } + + get icon(): string { + return this._icon; + } + + get remoteInputs(): RemoteInput[] { + return this._remoteInputs; + } + + get semanticAction(): ?SemanticActionType { + return this._semanticAction; + } + + get showUserInterface(): ?boolean { + return this._showUserInterface; + } + + get title(): string { + return this._title; + } + + /** + * + * @param remoteInput + * @returns {AndroidAction} + */ + addRemoteInput(remoteInput: RemoteInput): AndroidAction { + if (!(remoteInput instanceof RemoteInput)) { + throw new Error( + `AndroidAction:addRemoteInput expects an 'RemoteInput' but got type ${typeof remoteInput}` + ); + } + this._remoteInputs.push(remoteInput); + return this; + } + + /** + * + * @param allowGeneratedReplies + * @returns {AndroidAction} + */ + setAllowGenerateReplies(allowGeneratedReplies: boolean): AndroidAction { + this._allowGeneratedReplies = allowGeneratedReplies; + return this; + } + + /** + * + * @param semanticAction + * @returns {AndroidAction} + */ + setSemanticAction(semanticAction: SemanticActionType): AndroidAction { + if (!Object.values(SemanticAction).includes(semanticAction)) { + throw new Error( + `AndroidAction:setSemanticAction Invalid Semantic Action: ${semanticAction}` + ); + } + this._semanticAction = semanticAction; + return this; + } + + /** + * + * @param showUserInterface + * @returns {AndroidAction} + */ + setShowUserInterface(showUserInterface: boolean): AndroidAction { + this._showUserInterface = showUserInterface; + return this; + } + + build(): NativeAndroidAction { + if (!this._action) { + throw new Error('AndroidAction: Missing required `action` property'); + } else if (!this._icon) { + throw new Error('AndroidAction: Missing required `icon` property'); + } else if (!this._title) { + throw new Error('AndroidAction: Missing required `title` property'); + } + + return { + action: this._action, + allowGeneratedReplies: this._allowGeneratedReplies, + icon: this._icon, + remoteInputs: this._remoteInputs.map(remoteInput => remoteInput.build()), + semanticAction: this._semanticAction, + showUserInterface: this._showUserInterface, + title: this._title, + }; + } +} + +export const fromNativeAndroidAction = ( + nativeAction: NativeAndroidAction +): AndroidAction => { + const action = new AndroidAction( + nativeAction.action, + nativeAction.icon, + nativeAction.title + ); + if (nativeAction.allowGeneratedReplies) { + action.setAllowGenerateReplies(nativeAction.allowGeneratedReplies); + } + if (nativeAction.remoteInputs) { + nativeAction.remoteInputs.forEach(remoteInput => { + action.addRemoteInput(fromNativeAndroidRemoteInput(remoteInput)); + }); + } + if (nativeAction.semanticAction) { + action.setSemanticAction(nativeAction.semanticAction); + } + if (nativeAction.showUserInterface) { + action.setShowUserInterface(nativeAction.showUserInterface); + } + + return action; +}; diff --git a/lib/modules/notifications/AndroidChannel.js b/lib/modules/notifications/AndroidChannel.js index 5e3f6cf0..f64cbf9e 100644 --- a/lib/modules/notifications/AndroidChannel.js +++ b/lib/modules/notifications/AndroidChannel.js @@ -2,6 +2,7 @@ * @flow * AndroidChannel representation wrapper */ +import { Importance, Visibility } from './types'; import type { ImportanceType, VisibilityType } from './types'; type NativeAndroidChannel = {| @@ -31,6 +32,15 @@ export default class AndroidChannel { _sound: string | void; _vibrationPattern: number[] | void; + constructor(channelId: string, name: string, importance: ImportanceType) { + if (!Object.values(Importance).includes(importance)) { + throw new Error(`AndroidChannel() Invalid Importance: ${importance}`); + } + this._channelId = channelId; + this._name = name; + this._importance = importance; + } + get bypassDnd(): ?boolean { return this._bypassDnd; } @@ -85,16 +95,6 @@ export default class AndroidChannel { return this; } - /** - * - * @param channelId - * @returns {AndroidChannel} - */ - setChannelId(channelId: string): AndroidChannel { - this._channelId = channelId; - return this; - } - /** * * @param description @@ -115,16 +115,6 @@ export default class AndroidChannel { return this; } - /** - * - * @param importance - * @returns {AndroidChannel} - */ - setImportance(importance: ImportanceType): AndroidChannel { - this._importance = importance; - return this; - } - /** * * @param lightColor @@ -143,20 +133,15 @@ export default class AndroidChannel { setLockScreenVisibility( lockScreenVisibility: VisibilityType ): AndroidChannel { + if (!Object.values(Visibility).includes(lockScreenVisibility)) { + throw new Error( + `AndroidChannel:setLockScreenVisibility Invalid Visibility: ${lockScreenVisibility}` + ); + } this._lockScreenVisibility = lockScreenVisibility; return this; } - /** - * - * @param name - * @returns {AndroidChannel} - */ - setName(name: string): AndroidChannel { - this._name = name; - return this; - } - /** * * @param showBadge diff --git a/lib/modules/notifications/AndroidChannelGroup.js b/lib/modules/notifications/AndroidChannelGroup.js index fb37a527..3485c276 100644 --- a/lib/modules/notifications/AndroidChannelGroup.js +++ b/lib/modules/notifications/AndroidChannelGroup.js @@ -8,10 +8,15 @@ type NativeAndroidChannelGroup = {| name: string, |}; -export default class AndroidChannel { +export default class AndroidChannelGroup { _groupId: string; _name: string; + constructor(groupId: string, name: string) { + this._groupId = groupId; + this._name = name; + } + get groupId(): string { return this._groupId; } @@ -20,26 +25,6 @@ export default class AndroidChannel { return this._name; } - /** - * - * @param groupId - * @returns {AndroidChannel} - */ - setGroupId(groupId: string): AndroidChannel { - this._groupId = groupId; - return this; - } - - /** - * - * @param name - * @returns {AndroidChannel} - */ - setName(name: string): AndroidChannel { - this._name = name; - return this; - } - build(): NativeAndroidChannelGroup { if (!this._groupId) { throw new Error( diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js index 4fadb223..f67b3d3f 100644 --- a/lib/modules/notifications/AndroidNotification.js +++ b/lib/modules/notifications/AndroidNotification.js @@ -2,7 +2,8 @@ * @flow * AndroidNotification representation wrapper */ -import { Category } from './types'; +import AndroidAction, { fromNativeAndroidAction } from './AndroidAction'; +import { BadgeIconType, Category, GroupAlert, Priority } from './types'; import type Notification from './Notification'; import type { BadgeIconTypeType, @@ -18,8 +19,7 @@ import type { } from './types'; export default class AndroidNotification { - // TODO optional fields - // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) + _actions: AndroidAction[]; _autoCancel: boolean | void; _badgeIconType: BadgeIconTypeType | void; _category: CategoryType | void; @@ -70,6 +70,9 @@ export default class AndroidNotification { this._notification = notification; if (data) { + this._actions = data.actions + ? data.actions.map(action => fromNativeAndroidAction(action)) + : []; this._autoCancel = data.autoCancel; this._badgeIconType = data.badgeIconType; this._category = data.category; @@ -106,12 +109,17 @@ export default class AndroidNotification { } // Defaults + this._actions = this._actions || []; this._people = this._people || []; this._smallIcon = this._smallIcon || { icon: 'ic_launcher', }; } + get actions(): AndroidAction[] { + return this._actions; + } + get autoCancel(): ?boolean { return this._autoCancel; } @@ -240,6 +248,21 @@ export default class AndroidNotification { return this._when; } + /** + * + * @param action + * @returns {Notification} + */ + addAction(action: AndroidAction): Notification { + if (!(action instanceof AndroidAction)) { + throw new Error( + `AndroidNotification:addAction expects an 'AndroidAction' but got type ${typeof action}` + ); + } + this._actions.push(action); + return this._notification; + } + /** * * @param person @@ -266,6 +289,11 @@ export default class AndroidNotification { * @returns {Notification} */ setBadgeIconType(badgeIconType: BadgeIconTypeType): Notification { + if (!Object.values(BadgeIconType).includes(badgeIconType)) { + throw new Error( + `AndroidNotification:setBadgeIconType Invalid BadgeIconType: ${badgeIconType}` + ); + } this._badgeIconType = badgeIconType; return this._notification; } @@ -277,7 +305,9 @@ export default class AndroidNotification { */ setCategory(category: CategoryType): Notification { if (!Object.values(Category).includes(category)) { - throw new Error(`AndroidNotification: Invalid Category: ${category}`); + throw new Error( + `AndroidNotification:setCategory Invalid Category: ${category}` + ); } this._category = category; return this._notification; @@ -359,6 +389,11 @@ export default class AndroidNotification { * @returns {Notification} */ setGroupAlertBehaviour(groupAlertBehaviour: GroupAlertType): Notification { + if (!Object.values(GroupAlert).includes(groupAlertBehaviour)) { + throw new Error( + `AndroidNotification:setGroupAlertBehaviour Invalid GroupAlert: ${groupAlertBehaviour}` + ); + } this._groupAlertBehaviour = groupAlertBehaviour; return this._notification; } @@ -445,6 +480,11 @@ export default class AndroidNotification { * @returns {Notification} */ setPriority(priority: PriorityType): Notification { + if (!Object.values(Priority).includes(priority)) { + throw new Error( + `AndroidNotification:setPriority Invalid Priority: ${priority}` + ); + } this._priority = priority; return this._notification; } @@ -596,7 +636,7 @@ export default class AndroidNotification { } return { - // TODO actions: Action[], + actions: this._actions.map(action => action.build()), autoCancel: this._autoCancel, badgeIconType: this._badgeIconType, category: this._category, diff --git a/lib/modules/notifications/AndroidRemoteInput.js b/lib/modules/notifications/AndroidRemoteInput.js new file mode 100644 index 00000000..29b29fbb --- /dev/null +++ b/lib/modules/notifications/AndroidRemoteInput.js @@ -0,0 +1,123 @@ +/** + * @flow + * AndroidRemoteInput representation wrapper + */ + +import type { AndroidAllowDataType, NativeAndroidRemoteInput } from './types'; + +export default class AndroidRemoteInput { + _allowedDataTypes: AndroidAllowDataType[]; + _allowFreeFormInput: boolean | void; + _choices: string[]; + _label: string | void; + _resultKey: string; + + constructor(resultKey: string) { + this._allowedDataTypes = []; + this._choices = []; + this._resultKey = resultKey; + } + + get allowedDataTypes(): AndroidAllowDataType[] { + return this._allowedDataTypes; + } + + get allowFreeFormInput(): ?boolean { + return this._allowFreeFormInput; + } + + get choices(): string[] { + return this._choices; + } + + get label(): ?string { + return this._label; + } + + get resultKey(): string { + return this._resultKey; + } + + /** + * + * @param mimeType + * @param allow + * @returns {AndroidRemoteInput} + */ + setAllowDataType(mimeType: string, allow: boolean): AndroidRemoteInput { + this._allowedDataTypes.push({ + allow, + mimeType, + }); + return this; + } + + /** + * + * @param allowFreeFormInput + * @returns {AndroidRemoteInput} + */ + setAllowFreeFormInput(allowFreeFormInput: boolean): AndroidRemoteInput { + this._allowFreeFormInput = allowFreeFormInput; + return this; + } + + /** + * + * @param choices + * @returns {AndroidRemoteInput} + */ + setChoices(choices: string[]): AndroidRemoteInput { + this._choices = choices; + return this; + } + + /** + * + * @param label + * @returns {AndroidRemoteInput} + */ + setLabel(label: string): AndroidRemoteInput { + this._label = label; + return this; + } + + build(): NativeAndroidRemoteInput { + if (!this._resultKey) { + throw new Error( + 'AndroidRemoteInput: Missing required `resultKey` property' + ); + } + + return { + allowedDataTypes: this._allowedDataTypes, + allowFreeFormInput: this._allowFreeFormInput, + choices: this._choices, + label: this._label, + resultKey: this._resultKey, + }; + } +} + +export const fromNativeAndroidRemoteInput = ( + nativeRemoteInput: NativeAndroidRemoteInput +): AndroidRemoteInput => { + const remoteInput = new AndroidRemoteInput(nativeRemoteInput.resultKey); + if (nativeRemoteInput.allowDataType) { + for (let i = 0; i < nativeRemoteInput.allowDataType.length; i++) { + const allowDataType = nativeRemoteInput.allowDataType[i]; + remoteInput.setAllowDataType(allowDataType.mimeType, allowDataType.allow); + } + } + if (nativeRemoteInput.allowFreeFormInput) { + remoteInput.setAllowFreeFormInput(nativeRemoteInput.allowFreeFormInput); + } + if (nativeRemoteInput.choices) { + remoteInput.setChoices(nativeRemoteInput.choices); + } + if (nativeRemoteInput.label) { + remoteInput.setLabel(nativeRemoteInput.label); + } + + return remoteInput; +}; diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js index 37ccb2bc..9385c27e 100644 --- a/lib/modules/notifications/Notification.js +++ b/lib/modules/notifications/Notification.js @@ -9,10 +9,11 @@ import { generatePushID, isObject } from '../../utils'; import type { NativeNotification } from './types'; -export type NotificationOpen = { +export type NotificationOpen = {| action: string, notification: Notification, -}; + results?: { [string]: string }, +|}; export default class Notification { // iOS 8/9 | 10+ | Android diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 662d137a..1a4fc09f 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -7,9 +7,11 @@ import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; +import AndroidAction from './AndroidAction'; import AndroidChannel from './AndroidChannel'; import AndroidChannelGroup from './AndroidChannelGroup'; import AndroidNotifications from './AndroidNotifications'; +import AndroidRemoteInput from './AndroidRemoteInput'; import Notification from './Notification'; import { BadgeIconType, @@ -18,6 +20,7 @@ import { GroupAlert, Importance, Priority, + SemanticAction, Visibility, } from './types'; @@ -99,6 +102,7 @@ export default class Notifications extends ModuleBase { SharedEventEmitter.emit('onNotificationOpened', { action: notificationOpen.action, notification: new Notification(notificationOpen.notification), + results: notificationOpen.results, }); } ); @@ -166,6 +170,7 @@ export default class Notifications extends ModuleBase { return { action: notificationOpen.action, notification: new Notification(notificationOpen.notification), + results: notificationOpen.results, }; } return null; @@ -295,6 +300,7 @@ export default class Notifications extends ModuleBase { export const statics = { Android: { + Action: AndroidAction, BadgeIconType, Category, Channel: AndroidChannel, @@ -303,6 +309,8 @@ export const statics = { GroupAlert, Importance, Priority, + RemoteInput: AndroidRemoteInput, + SemanticAction, Visibility, }, Notification, diff --git a/lib/modules/notifications/types.js b/lib/modules/notifications/types.js index 5a77c74a..1441d8e3 100644 --- a/lib/modules/notifications/types.js +++ b/lib/modules/notifications/types.js @@ -1,7 +1,6 @@ /** * @flow */ - export const BadgeIconType = { Large: 2, None: 0, @@ -57,6 +56,20 @@ export const Priority = { Min: -2, }; +export const SemanticAction = { + Archive: 5, + Call: 10, + Delete: 4, + MarkAsRead: 2, + MarkAsUnread: 3, + Mute: 6, + None: 0, + Reply: 1, + ThumbsDown: 9, + ThumbsUp: 8, + Unmute: 7, +}; + export const Visibility = { Private: 0, Public: 1, @@ -69,27 +82,51 @@ export type DefaultsType = $Values; export type GroupAlertType = $Values; export type ImportanceType = $Values; export type PriorityType = $Values; +export type SemanticActionType = $Values; export type VisibilityType = $Values; -export type Lights = { +export type Lights = {| argb: number, onMs: number, offMs: number, -}; +|}; -export type Progress = { +export type Progress = {| max: number, progress: number, indeterminate: boolean, -}; +|}; -export type SmallIcon = { +export type SmallIcon = {| icon: string, level?: number, +|}; + +export type AndroidAllowDataType = { + allow: boolean, + mimeType: string, }; +export type NativeAndroidRemoteInput = {| + allowedDataTypes: AndroidAllowDataType[], + allowFreeFormInput?: boolean, + choices: string[], + label?: string, + resultKey: string, +|}; + +export type NativeAndroidAction = {| + action: string, + allowGeneratedReplies?: boolean, + icon: string, + remoteInputs: NativeAndroidRemoteInput[], + semanticAction?: SemanticActionType, + showUserInterface?: boolean, + title: string, +|}; + export type NativeAndroidNotification = {| - // TODO actions: Action[], + actions?: NativeAndroidAction[], autoCancel?: boolean, badgeIconType?: BadgeIconTypeType, category?: CategoryType, @@ -154,11 +191,11 @@ export type NativeIOSNotification = {| threadIdentifier?: string, |}; -export type Schedule = { +export type Schedule = {| exact?: boolean, fireDate: number, repeatInterval?: 'minute' | 'hour' | 'day' | 'week', -}; +|}; export type NativeNotification = {| android?: NativeAndroidNotification, @@ -175,4 +212,5 @@ export type NativeNotification = {| export type NativeNotificationOpen = {| action: string, notification: NativeNotification, + results?: { [string]: string }, |}; diff --git a/tests/src/firebase.js b/tests/src/firebase.js index 79320804..dd3c9c72 100644 --- a/tests/src/firebase.js +++ b/tests/src/firebase.js @@ -37,22 +37,35 @@ const init = async () => { console.log('onNotificationDisplayed: ', notification); }); // RNfirebase.instanceid().delete(); - const channel = new RNfirebase.notifications.Android.Channel(); - channel - .setChannelId('test') - .setName('test') - .setImportance(RNfirebase.notifications.Android.Importance.Max) - .setDescription('test channel'); + const channel = new RNfirebase.notifications.Android.Channel( + 'test', + 'test', + RNfirebase.notifications.Android.Importance.Max + ); + channel.setDescription('test channel'); RNfirebase.notifications().android.createChannel(channel); + const remoteInput = new RNfirebase.notifications.Android.RemoteInput( + 'inputText' + ); + remoteInput.setLabel('Message'); + const action = new RNfirebase.notifications.Android.Action( + 'test_action', + 'ic_launcher', + 'My Test Action' + ); + action.addRemoteInput(remoteInput); + const notification = new RNfirebase.notifications.Notification(); notification .setTitle('Test title') .setBody('Test body') .setNotificationId('displayed') + .android.addAction(action) .android.setChannelId('test') .android.setClickAction('action') .android.setPriority(RNfirebase.notifications.Android.Priority.Max); + const date = new Date(); date.setMinutes(date.getMinutes() + 1); setTimeout(() => {