From 60087e6b4d2552d08fb2640ceda26723f00c151f Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 29 Aug 2017 12:16:34 -0300 Subject: [PATCH] Enable replying from the push notification (#871) * Enable replying from the push notification * Fix Android and iOS flow * Own review --- android/app/src/main/AndroidManifest.xml | 7 +- .../rnbeta/CustomPushNotification.java | 59 ++++++-- .../rnbeta/NotificationDismissReceiver.java | 14 -- .../rnbeta/NotificationDismissService.java | 23 ++++ .../rnbeta/NotificationReplyService.java | 49 +++++++ .../drawable-hdpi/ic_notif_action_reply.png | Bin 0 -> 630 bytes .../drawable-mdpi/ic_notif_action_reply.png | Bin 0 -> 508 bytes .../drawable-xhdpi/ic_notif_action_reply.png | Bin 0 -> 925 bytes .../drawable-xxhdpi/ic_notif_action_reply.png | Bin 0 -> 1377 bytes .../ic_notif_action_reply.png | Bin 0 -> 2075 bytes app/components/badge.js | 4 +- app/mattermost.js | 127 ++++++++++++++---- .../push_notifications.android.js | 29 +++- .../push_notifications.ios.js | 47 ++++++- fastlane/Fastfile | 8 +- ios/Mattermost.xcodeproj/project.pbxproj | 3 - ios/Mattermost/AppDelegate.m | 12 ++ ios/Mattermost/Info.plist | 10 +- yarn.lock | 4 +- 19 files changed, 324 insertions(+), 72 deletions(-) delete mode 100644 android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissReceiver.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java create mode 100644 android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java create mode 100644 android/app/src/main/res/drawable-hdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_notif_action_reply.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_notif_action_reply.png diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6cc5eb9de..8e1e59741 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -45,7 +45,12 @@ - + + diff --git a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java index 1812ef689..e87e25acd 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/CustomPushNotification.java @@ -11,10 +11,13 @@ import android.os.Bundle; import android.os.Build; import android.app.Notification; import android.app.NotificationManager; +import android.app.RemoteInput; import java.util.LinkedHashMap; import java.util.ArrayList; +import java.lang.reflect.Field; import com.wix.reactnativenotifications.core.notification.PushNotification; +import com.wix.reactnativenotifications.core.NotificationIntentAdapter; import com.wix.reactnativenotifications.core.AppLaunchHelper; import com.wix.reactnativenotifications.core.AppLifecycleFacade; import com.wix.reactnativenotifications.core.JsIOHelper; @@ -27,18 +30,25 @@ public class CustomPushNotification extends PushNotification { public static final int MESSAGE_NOTIFICATION_ID = 435345; public static final String GROUP_KEY_MESSAGES = "mm_group_key_messages"; public static final String NOTIFICATION_ID = "notificationId"; + public static final String KEY_TEXT_REPLY = "CAN_REPLY"; + public static final String NOTIFICATION_REPLIED_EVENT_NAME = "notificationReplied"; + private static LinkedHashMap channelIdToNotificationCount = new LinkedHashMap(); private static LinkedHashMap> channelIdToNotification = new LinkedHashMap>(); + private static AppLifecycleFacade lifecycleFacade; + private static Context context; public CustomPushNotification(Context context, Bundle bundle, AppLifecycleFacade appLifecycleFacade, AppLaunchHelper appLaunchHelper, JsIOHelper jsIoHelper) { super(context, bundle, appLifecycleFacade, appLaunchHelper, jsIoHelper); + this.context = context; } - public static void clearNotification(int notificationId) { + public static void clearNotification(int notificationId, String channelId) { if (notificationId != -1) { - String channelId = String.valueOf(notificationId); channelIdToNotificationCount.remove(channelId); channelIdToNotification.remove(channelId); + final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(notificationId); } } @@ -108,8 +118,9 @@ public class CustomPushNotification extends PushNotification { title = mContext.getPackageManager().getApplicationLabel(appInfo).toString(); } - int notificationId = bundle.getString("channel_id").hashCode(); String channelId = bundle.getString("channel_id"); + String postId = bundle.getString("post_id"); + int notificationId = channelId.hashCode(); String message = bundle.getString("message"); String subText = bundle.getString("subText"); String numberString = bundle.getString("badge"); @@ -143,11 +154,7 @@ public class CustomPushNotification extends PushNotification { ApplicationBadgeHelper.instance.setApplicationIconBadgeNumber(mContext.getApplicationContext(), Integer.parseInt(numberString)); } - int numMessages = 0; - Object objCount = channelIdToNotificationCount.get(channelId); - if (objCount != null) { - numMessages = (Integer)objCount; - } + int numMessages = getMessageCountInChannel(channelId); notification .setGroupSummary(true) @@ -173,19 +180,40 @@ public class CustomPushNotification extends PushNotification { style.setBigContentTitle(title); notification.setStyle(style) .setContentTitle(summaryTitle); -// .setNumber(numMessages); } // Let's add a delete intent when the notification is dismissed - Intent delIntent = new Intent(mContext, NotificationDismissReceiver.class); + Intent delIntent = new Intent(mContext, NotificationDismissService.class); delIntent.putExtra(NOTIFICATION_ID, notificationId); - PendingIntent deleteIntent = PendingIntent.getBroadcast(mContext, 0, delIntent, 0); + PendingIntent deleteIntent = NotificationIntentAdapter.createPendingNotificationIntent(mContext, delIntent, mNotificationProps); notification.setDeleteIntent(deleteIntent); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { notification.setGroup(GROUP_KEY_MESSAGES); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && postId != null) { + Intent replyIntent = new Intent(mContext, NotificationReplyService.class); + replyIntent.setAction(KEY_TEXT_REPLY); + replyIntent.putExtra(NOTIFICATION_ID, notificationId); + replyIntent.putExtra("pushNotification", bundle); + PendingIntent replyPendingIntent = PendingIntent.getService(mContext, 103, replyIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + RemoteInput remoteInput = new RemoteInput.Builder(KEY_TEXT_REPLY) + .setLabel("Reply") + .build(); + + Notification.Action replyAction = new Notification.Action.Builder( + R.drawable.ic_notif_action_reply, "Reply", replyPendingIntent) + .addRemoteInput(remoteInput) + .setAllowGeneratedReplies(true) + .build(); + + notification + .setShowWhen(true) + .addAction(replyAction); + } + Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId); if (largeIconResId != 0 && (largeIcon != null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { notification.setLargeIcon(largeIconBitmap); @@ -202,6 +230,15 @@ public class CustomPushNotification extends PushNotification { mJsIOHelper.sendEventToJS(NOTIFICATION_RECEIVED_EVENT_NAME, mNotificationProps.asBundle(), mAppLifecycleFacade.getRunningReactContext()); } + public static Integer getMessageCountInChannel(String channelId) { + Object objCount = channelIdToNotificationCount.get(channelId); + if (objCount != null) { + return (Integer)objCount; + } + + return 0; + } + private void cancelNotification(Bundle data, int notificationId) { final String channelId = data.getString("channel_id"); diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissReceiver.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissReceiver.java deleted file mode 100644 index be0706b39..000000000 --- a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissReceiver.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.mattermost.rnbeta; - -import android.content.Context; -import android.content.Intent; -import android.content.BroadcastReceiver; - - -public class NotificationDismissReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); - CustomPushNotification.clearNotification(notificationId); - } -} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java new file mode 100644 index 000000000..48406f275 --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationDismissService.java @@ -0,0 +1,23 @@ +package com.mattermost.rnbeta; + +import android.content.Context; +import android.content.Intent; +import android.app.IntentService; +import android.os.Bundle; + +import com.wix.reactnativenotifications.core.NotificationIntentAdapter; + +public class NotificationDismissService extends IntentService { + + public NotificationDismissService() { + super("notificationDismissService"); + } + + @Override + protected void onHandleIntent(Intent intent) { + Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); + int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); + String channelId = bundle.getString("channel_id"); + CustomPushNotification.clearNotification(notificationId, channelId); + } +} diff --git a/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java new file mode 100644 index 000000000..cadc2abcb --- /dev/null +++ b/android/app/src/main/java/com/mattermost/rnbeta/NotificationReplyService.java @@ -0,0 +1,49 @@ +package com.mattermost.rnbeta; + +import android.content.Context; +import android.content.Intent; +import android.app.NotificationManager; +import android.app.RemoteInput; +import android.os.Bundle; +import android.support.annotation.Nullable; + +import com.facebook.react.HeadlessJsTaskService; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.jstasks.HeadlessJsTaskConfig; + + +import com.wix.reactnativenotifications.core.NotificationIntentAdapter; + +public class NotificationReplyService extends HeadlessJsTaskService { + + @Override + protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) { + if (CustomPushNotification.KEY_TEXT_REPLY.equals(intent.getAction())) { + CharSequence message = getReplyMessage(intent); + + Bundle bundle = NotificationIntentAdapter.extractPendingNotificationDataFromIntent(intent); + String channelId = bundle.getString("channel_id"); + bundle.putCharSequence("text", message); + bundle.putInt("msg_count", CustomPushNotification.getMessageCountInChannel(channelId)); + + int notificationId = intent.getIntExtra(CustomPushNotification.NOTIFICATION_ID, -1); + CustomPushNotification.clearNotification(notificationId, channelId); + + return new HeadlessJsTaskConfig( + "notificationReplied", + Arguments.fromBundle(bundle), + 5000); + + } + + return null; + } + + private CharSequence getReplyMessage(Intent intent) { + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + return remoteInput.getCharSequence(CustomPushNotification.KEY_TEXT_REPLY); + } + return null; + } +} diff --git a/android/app/src/main/res/drawable-hdpi/ic_notif_action_reply.png b/android/app/src/main/res/drawable-hdpi/ic_notif_action_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..d7d3365f50926c54ce1d55b4a101007713d95779 GIT binary patch literal 630 zcmV-+0*U>JP)ISq8kK(W!v`2RsrXkcYWVK&~-hN%jG_74KOQCqFV)! zIAd&@d6cn~GE*oNHm@K~oFJN}eV0;x3&Ze7uh;uktJQ*a?YdaxNZj+h14IO+6hv3f zInXJD0LIuE0DKOD;GJ#Tr%9kGU{*Zo1$v%JrRWS1@ylp5a!RGrpJnf1Bt94n7BZi& zWd8mfG~k@)v)SzXI0ym0`o8}(No>_~DJ50(#!y9eb9=5JU*^ zHlNQ|W@Dyn5JAE)Om9*NSw<~AyD2Zah#-^6qyqpORif+q*>E_#Td7q3Qu3E((F(|G zrPNPEGyvchBHmU?-2;G!0ASABW}4uv?=REX0@5ajSK?Kn>t-UUQK(6ajqjU_zC?x*25M(?a->g=v zzpt`S>h*fsG|j_Az*R)&W1G$9lbr%$+jNtRV~5?g1s8_h9+2d~|KCOb0A!jR`8!nY Q_W%F@07*qoM6N<$f?2XA>i_@% literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_notif_action_reply.png b/android/app/src/main/res/drawable-mdpi/ic_notif_action_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..a144ef6062bcca65935e6746a47a3d572ca54cce GIT binary patch literal 508 zcmVX1^@s6IQ*`u0005QNkljic3k4CYK)OvLGxNy?bDSpe?zr}*8HV}3_rCA(c!VP!I$}SL0kBwue!qXu zTKlzJF7NCo@|=iyvoYqGbFLJ}@rOeJbi3X2MD%Vv9-op@UO5y%uh+ZD%uh$7(K%*D z7={&V?b|4dJ`)iyn`FAeUS{VUhzP}E5mL&Gncp+>=3p>*p|$??FA)7{-pI1-!XlPJ zp#abG*z>&C&bcR16n)%Vmjk?+OeVLiwNvJN{6)7A0>1D6G{%&w)#~;PggTwhsdSCAyr{L#c72ehX$ipk41|at z1NgzrX8@c9a2>!(PMWW%lzJ4$@$v0000F|9LL}9&3p6ST6nNSbge*wf-(>S4|a&?V04Je=q|a-F0Gn*2p)l~|5nsM@l>}0 zZ`DDP0)q+<9U?nK1RX^-J(Qx(%zN?Mc4S@M+1=FjGIJT0+4+4x@B90{-#j1(e8BNGWG2rE|uZpOn)1#l^*sN-6h$L%C%e4h{|;5<*Nn zjuar`M%3;U?pg18U+w=Uul~W-9Kj@ASV%f5+m&Ta!SS&VIss`+p zc)k#}h;#lG9c^!Ke-Z=%qoboQ9LH&?#PW2}#J z-rv^N=4{E0|6@NPa_v}PjNL2zkZqX*2%)u}0f4%FYR4xb#D#npww(w1`}@0$F%JSD zei>tM&*ONG0KnmLVR3j#DX+xi@z=idJ03{T3D5J=0MNWc@f$roJyU*x;o;#r$8nBo zt&ah~39WTArS!Zp<|F`Eh3`iqk$Km3?{su@e8w6@PejEagb=OCWU}bTXmWCrXEK>n zj4_12NC>%VjG_K6qLk`LB+|FEwDb_icHoh$x3{+e0J53zS6m7pKjBKLS|LP|5ORwU zQtv;rZTkyj>~2&n}C*S75k$z(EJ z5>rIgpS*4;R)Dsdnm2EVk9<6Odd0|3Wu+n&WNlmzgLn%L+QW(0w>{Kt+Jdtp#z}r$ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notif_action_reply.png b/android/app/src/main/res/drawable-xxhdpi/ic_notif_action_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..c5cd53e89fc5428f9372aa216c1dd68d4ebf5049 GIT binary patch literal 1377 zcmV-n1)lneP)mQZqm*a1larNd(fq$QMGAa+1f zKX;o2Ge{QfeiW$)^HmAQ>P9z>~Ku z>qF1;&NId~GMUWNlKBJ<4-Fuc-{0S#s@Lm)h#QXM33;`WmFH#Cdi6bkP+rtLFB?Q`ln?*pJ&?_2`?Cjx@_T!G2Q}ZC zyy$_xY~ohsTWh|3-tiLZI|rJ{H|BhM&36F!o)B_TQItO^rGMMD{ZHVb9wMOYIp9}$ zv8SRa%rK095wVPjJo!6__*tb=`O!bnrA#LC zvqatpq4xm@Z%i zmKkq$!!Z5;fTdI_^+6zjwY9Z(JkN8)Q)ST2qr6|&b>1KI+uPe~uIqY!K(JIQodSUC z05IVv;JEM6b$wq|)lY2OzUXJKMh}!qrAYv|)-Mo*5YTn~aSsi@-lOaKuK;k?ah#u8|9<3qA>qVP zgZcl7=b=BD2-5o;2VNKJ9D5VcF#?Ao1a{zF%33}vHF91->vfj0AI~xc< z^aOXJ7lfv18}sw?fqvBI^Z7AC$g7mn7ZC9g#@KML!J%ndMc4I{wry_;h$9^kH*zNc zpleU?7h4Mp3xNQ{+QnjVm@)P+W9%Fv&I3TB3_D$>X?{97I{LEsQREvyI6*1x*b_1U zt%)uzEuGLb?JdUG^N2Xo4xwq{#qreWd_k?I{{$41T z%Z2KJUL)0YT`iGFOj1g(AR_OR#tQ?}G@neT)8B*!;G5?2`AJn(8`FtTp|DNt=S(OT zixZU6>xg)YLs*t|ZD(iaMJWK$6O6GNs;d5S91gU?H6i#A;lkZAP4l!gfN+8kGMmfg zUh$)*%ss*hMNv)=LY|2PfG6a-?uhHU$My!U-HCd=e$%q7*CPXvQ8i5L^78T%hGAS7 zSO9)jY;A2#4J?3gEd~fvzfs1-4n(PDn6+XD!l)KyOzc3EYKB=Wb|8#uQO3j$M5$(& jwPFXts1{|+z&r3CsaiS6yGz|M00000NkvXXu0mjft-*u* literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notif_action_reply.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notif_action_reply.png new file mode 100644 index 0000000000000000000000000000000000000000..36600d49a4f9407ab90edb54f59c077938400da7 GIT binary patch literal 2075 zcmV+$2;}#PP)0e7w2tef6l#;Um>3GBEq%ya!LCF5;FnU;v`z(HdCh9Ik}VrF z5AH^*7K_@csGv5K5YoDo(ozcPOKC`(KJ+yWiTe?fQp=XKl51DmkC}VQ5j$ZlYc;#; zS?x+Y3k!moy>rj+{?GZJdq)U;{=q(v9})nG0{Walk^+4$fTRc}DUhT(}MN!opD^nkkpd`-y0IWMrg209?L&S=+XK z%=i88RVtN3L*)#}>Hulwxm8Za|6bKDp+RjpPJZ4f-O*tr1eK0i~bRQ88* zJ}m#W8j#QDcU}>!2Y;lOSO1m(-~#}7R|xUfa=H9gHk)0J8l+R#BLUQ_ z{S^21Ew221bLO@<_zEFBA;c0Z0ukql=x>PlTLAcTI-S1PsbDu6cT)fyJOD)be3T0` zye_5WIgw>qtB6>#EbDzD`kn9lKP#0=|8iaTfwnSkI1K@C@I=J>`MnWUKFS4}x-Nv^ z*oi$kXe6z7_+Xki7e~IWv zMD)_);^J-Bb-lRfb02`f=WDfEct79N3f8y2ZKp${-z%law(a);;D^5N|3YhhYjAL| z78i&Q0kF^WVMef@Z^7r|noGlR7>;e*|FdoTbpW__@7}%F#>U3(#_`}a0KD>u$h*3B zd_InOG&KenO{LU70pR6owR$y|%dwq4)CW1|=jP^~HO5SMo_BcNVMbTwzfoFnv|4K} zlCN2oHFN6JsXsmp0px1W`}ta}cBt2b54#b&g=JaqNGUVvbovkNLcrC>L)UEX9r zAA4bot6;KqYOVhvrF=1+PG4;Y0=lld%W<4@LWuFL@cX!e55^HfuuIGdAqtsH=A~94 ztQEoO>FGVyYIPg{&H#XFb&y`TUu(^qt&62n>1r$e!iP3EH8u4yW6UT3dz{5TPfAmVdGv?p%4c6vOn3ASzjNhvitKR^GMX7&;f zw8E2U=Xt}~Z1%k<6>Yc~sw-g_5z|DZI=ymnk8fF4&9?0s zL>$XxGL_9BG~5kxO&}sxLVi2CgVuUUN_jey$-L4QfWRFpl}a7}*deyv$8ns$`@VlP zm&?7|X!4OKLP4Lf9U&kh4GawYI+MwKt!)6|K7okZ=@ZHT@M8e@RCp3ZvQcAg^wLY22WM19q-liWA?h^o@l|Etl?AfyoUyV&pPVPoT ze#h`pB07kO2LRw80I(bF27pHZU{@VVRJv9YoLtiPdY0Q^IC2V+c2 zeFDE6-3&mZh>FGH5i+fry_VqP>V{w^Eob%PJ|Qic3pNKUm)zv;=@B z%Y6d710p_8M2|LMVQm2jgPEP3wU?Hb4ieE9gb<%6qR#=qQ$)n)7|m?ijpI148)F97 z7sQqUgp|M*nU2~!@W05kqku-rC=?3(GL~z{5h28vi0BvqI8nJFrJUDVPwm^cZ~DZE z6Mk@iTLAdSToZiXXC*}S39$kQChoefC8a!|l=`AE<{OCk3=uh-D8eYEeh&a6!^6XG zJv0Cwz;)d{N~t^|HtZAPR>1m|W)P0!JZ_A61`$W<>ex^aQp(%5ZI5KL*_UGhP=~Nf zDRr(9giZqp0^y^*8#it|CWOcWz+eNXBBj(d-}f(!j*iapZ`)3RFk)7M)_UzcVS~NH z^7#1pmi~uGp-}ibn-(H|l88h&EiB7=TPgM3GiT1c5gUL|O;|fm2)lz2q9ZjRs^}F8 zg-`Q=HzKlYaMQKs7WHGaQ>j$yCwyiQivohlg!=?zOmLpCwr*_*Kf2=@tmp1?}zmI49@+(RrZEDUjNAff{dK}xxpN~OL!G&J;P>;S@j z0ugbau(H)PV10TNi^WGg&-*qaa)Er3uMZ3i4BfeNXSO2%!hHe}z0hp{b!G1Meg9jC zm;-stT}p-?D% zxvv4l+6uRthrM!tqJUmmiEhm!Q9!rI_sVk<1@y{FbZZ`o0=h-MSDu?FpjTF+Tk}X1 z&@J-4^4vrLy|NPBnn$95ZjtYm=Ozm1m6hn$Jo;V%{|EE{4|8R4M=t;X002ovPDHLk FV1jeQ?12CP literal 0 HcmV?d00001 diff --git a/app/components/badge.js b/app/components/badge.js index c5a0ab008..2efa43ced 100644 --- a/app/components/badge.js +++ b/app/components/badge.js @@ -91,7 +91,7 @@ export default class Badge extends PureComponent { setTimeout(() => { this.setNativeProps({ style: { - display: 'flex' + opacity: 1 } }); }, 100); @@ -125,7 +125,7 @@ export default class Badge extends PureComponent { > {this.renderText()} diff --git a/app/mattermost.js b/app/mattermost.js index 8c048b7b2..e2b4c14e8 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -5,7 +5,7 @@ import 'babel-polyfill'; import Analytics from 'analytics-react-native'; import Orientation from 'react-native-orientation'; import {Provider} from 'react-redux'; -import {Navigation} from 'react-native-navigation'; +import {Navigation, NativeEventsReceiver} from 'react-native-navigation'; import {IntlProvider} from 'react-intl'; import { Alert, @@ -23,6 +23,7 @@ import {General} from 'mattermost-redux/constants'; import {setAppState, setDeviceToken, setServerVersion} from 'mattermost-redux/actions/general'; import {markChannelAsRead} from 'mattermost-redux/actions/channels'; import {logError} from 'mattermost-redux/actions/errors'; +import {createPost} from 'mattermost-redux/actions/posts'; import {logout} from 'mattermost-redux/actions/users'; import {close as closeWebSocket} from 'mattermost-redux/actions/websocket'; import {Client, Client4} from 'mattermost-redux/client'; @@ -149,6 +150,7 @@ export default class Mattermost { PushNotifications.configure({ onRegister: this.onRegisterDevice, onNotification: this.onPushNotification, + onReply: this.onPushNotificationReply, popInitialNotification: true, requestPermissions: true }); @@ -158,24 +160,30 @@ export default class Mattermost { const {dispatch, getState} = store; const isActive = appState === 'active'; setAppState(isActive)(dispatch, getState); - try { - if (!isActive && !this.inBackgroundSince) { - this.inBackgroundSince = Date.now(); - } else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) { - this.inBackgroundSince = null; - if (this.mdmEnabled) { - const config = await mattermostManaged.getConfig(); - const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; - if (authNeeded) { - const authenticated = await this.handleAuthentication(config.vendor); - if (!authenticated) { - mattermostManaged.quitApp(); + + if (isActive && this.shouldRelaunchonActive) { + this.launchApp(); + this.shouldRelaunchonActive = false; + } else { + try { + if (!isActive && !this.inBackgroundSince) { + this.inBackgroundSince = Date.now(); + } else if (isActive && this.inBackgroundSince && (Date.now() - this.inBackgroundSince) >= AUTHENTICATION_TIMEOUT) { + this.inBackgroundSince = null; + if (this.mdmEnabled) { + const config = await mattermostManaged.getConfig(); + const authNeeded = config.inAppPinCode && config.inAppPinCode === 'true'; + if (authNeeded) { + const authenticated = await this.handleAuthentication(config.vendor); + if (!authenticated) { + mattermostManaged.quitApp(); + } } } } + } catch (error) { + // do nothing } - } catch (error) { - // do nothing } }; @@ -347,11 +355,33 @@ export default class Mattermost { const state = store.getState(); if (state.views.root.hydrationComplete) { this.unsubscribeFromStore(); - this.handleManagedConfig().then((shouldStart) => { - if (shouldStart) { - this.startApp(); - } - }); + + const notification = PushNotifications.getNotification(); + if (notification) { + // If we have a notification means that the app was started cause of a reply + // and the app was not sitting in the background nor opened + const {data, text, badge} = notification; + this.onPushNotificationReply(data, text, badge); + PushNotifications.resetNotification(); + } + + if (Platform.OS === 'android') { + // In case of Android we need to handle the bridge being initialized by HeadlessJS + Promise.resolve(Navigation.isAppLaunched()).then((appLaunched) => { + if (appLaunched) { + this.launchApp(); // App is launched -> show UI + } else { + new NativeEventsReceiver().appLaunched(this.launchApp); // App hasn't been launched yet -> show the UI only when needed. + } + }); + } else if (AppState.currentState === 'background') { + // for IOS replying from push notification starts the app in the background + this.shouldRelaunchonActive = true; + this.configurePushNotifications(); + this.startFakeApp(); + } else { + this.launchApp(); + } } }; @@ -366,6 +396,7 @@ export default class Mattermost { } else { prefix = General.PUSH_NOTIFY_ANDROID_REACT_NATIVE; } + setDeviceToken(`${prefix}:${data.token}`)(dispatch, getState); this.isConfigured = true; }; @@ -407,6 +438,46 @@ export default class Mattermost { } }; + onPushNotificationReply = (data, text, badge, completed) => { + const {dispatch, getState} = store; + const state = getState(); + const {currentUserId} = state.entities.users; + + if (currentUserId) { + // one thing to note is that for android it will reply to the last post in the stack + const rootId = data.root_id || data.post_id; + const post = { + user_id: currentUserId, + channel_id: data.channel_id, + root_id: rootId, + parent_id: rootId, + message: text + }; + + if (!Client4.getUrl()) { + // Make sure the Client has the server url set + Client4.setUrl(state.entities.general.credentials.url); + } + + if (!Client.getToken()) { + // Make sure the Client has the server token set + Client4.setToken(state.entities.general.credentials.token); + } + + createPost(post)(dispatch, getState); + markChannelAsRead(data.channel_id)(dispatch, getState); + + if (badge >= 0) { + PushNotifications.setApplicationIconBadgeNumber(badge); + } + } + + if (completed) { + // You must call to completed(), otherwise the action will not be triggered + completed(); + } + }; + resetBadgeAndVersion = () => { const {dispatch, getState} = store; Client4.serverVersion = ''; @@ -426,6 +497,14 @@ export default class Mattermost { this.startApp('fade'); }; + launchApp = () => { + this.handleManagedConfig().then((shouldStart) => { + if (shouldStart) { + this.startApp(); + } + }); + }; + startFakeApp = async () => { return Navigation.startSingleScreenApp({ screen: { @@ -443,6 +522,10 @@ export default class Mattermost { }; startApp = (animationType = 'none') => { + if (!this.isConfigured) { + this.configurePushNotifications(); + } + Navigation.startSingleScreenApp({ screen: { screen: 'Root', @@ -457,9 +540,5 @@ export default class Mattermost { }, animationType }); - - if (!this.isConfigured) { - this.configurePushNotifications(); - } }; } diff --git a/app/push_notifications/push_notifications.android.js b/app/push_notifications/push_notifications.android.js index 6051c6c7b..051f5ad56 100644 --- a/app/push_notifications/push_notifications.android.js +++ b/app/push_notifications/push_notifications.android.js @@ -1,13 +1,16 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import {AppState} from 'react-native'; +import {AppRegistry, AppState} from 'react-native'; import {NotificationsAndroid, PendingNotifications} from 'react-native-notifications'; +import Notification from 'react-native-notifications/notification.android'; class PushNotification { constructor() { this.onRegister = null; this.onNotification = null; + this.onReply = null; + this.deviceNotification = null; NotificationsAndroid.setNotificationReceivedListener((notification) => { if (notification) { @@ -22,6 +25,21 @@ class PushNotification { this.handleNotification(data, true); } }); + + AppRegistry.registerHeadlessTask('notificationReplied', () => async (deviceNotification) => { + const notification = new Notification(deviceNotification); + const data = notification.getData(); + + if (this.onReply && AppState.currentState === 'background') { + this.onReply(data, data.text, parseInt(data.badge, 10) - parseInt(data.msg_count, 10)); + } else { + this.deviceNotification = { + data, + text: data.text, + badge: parseInt(data.badge, 10) - parseInt(data.msg_count, 10) + }; + } + }); } handleNotification = (data, userInteraction) => { @@ -41,6 +59,7 @@ class PushNotification { configure(options) { this.onRegister = options.onRegister; this.onNotification = options.onNotification; + this.onReply = options.onReply; NotificationsAndroid.refreshToken(); NotificationsAndroid.setRegistrationTokenUpdateListener((deviceToken) => { @@ -80,6 +99,14 @@ class PushNotification { setApplicationIconBadgeNumber(number) { NotificationsAndroid.setBadgesCount(number); } + + getNotification() { + return this.deviceNotification; + } + + resetNotification() { + this.deviceNotification = null; + } } export default new PushNotification(); diff --git a/app/push_notifications/push_notifications.ios.js b/app/push_notifications/push_notifications.ios.js index eef25fadf..7cea3c312 100644 --- a/app/push_notifications/push_notifications.ios.js +++ b/app/push_notifications/push_notifications.ios.js @@ -2,12 +2,18 @@ // See License.txt for license information. import {AppState} from 'react-native'; -import NotificationsIOS from 'react-native-notifications'; +import NotificationsIOS, {NotificationAction, NotificationCategory} from 'react-native-notifications'; + +const CATEGORY = 'CAN_REPLY'; +const REPLY_ACTION = 'REPLY_ACTION'; + +let replyCategory; class PushNotification { constructor() { this.onRegister = null; this.onNotification = null; + this.onReply = null; NotificationsIOS.addEventListener('notificationReceivedForeground', (notification) => { const info = { @@ -32,6 +38,20 @@ class PushNotification { }; this.handleNotification(info, false, true); }); + + const replyAction = new NotificationAction({ + activationMode: 'background', + title: 'Reply', + behavior: 'textInput', + authenticationRequired: true, + identifier: REPLY_ACTION + }, this.handleReply); + + replyCategory = new NotificationCategory({ + identifier: CATEGORY, + actions: [replyAction], + context: 'default' + }); } handleNotification = (data, foreground, userInteraction) => { @@ -48,9 +68,24 @@ class PushNotification { } }; + handleReply = (action, completed) => { + if (action.identifier === REPLY_ACTION) { + const data = action.notification.getData(); + const text = action.text; + const badge = parseInt(action.notification._badge, 10) - 1; //eslint-disable-line no-underscore-dangle + + if (this.onReply) { + this.onReply(data, text, badge, completed); + } + } else { + completed(); + } + }; + configure(options) { this.onRegister = options.onRegister; this.onNotification = options.onNotification; + this.onReply = options.onReply; NotificationsIOS.addEventListener('remoteNotificationsRegistered', (deviceToken) => { if (this.onRegister) { @@ -59,7 +94,7 @@ class PushNotification { }); if (options.requestPermissions) { - this.requestPermissions(); + this.requestPermissions([replyCategory]); } if (options.popInitialNotification) { @@ -67,8 +102,8 @@ class PushNotification { } } - requestPermissions = () => { - NotificationsIOS.requestPermissions(); + requestPermissions = (permissions) => { + NotificationsIOS.requestPermissions(permissions); }; localNotificationSchedule(notification) { @@ -91,6 +126,10 @@ class PushNotification { setApplicationIconBadgeNumber(number) { NotificationsIOS.setBadgesCount(number); } + + getNotification() { + return null; + } } export default new PushNotification(); diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 5fdc68639..8dc07e169 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -350,7 +350,13 @@ platform :android do ) find_replace_string( - path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationDismissReceiver.java', + path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationDismissService.java', + old_string: 'package com.mattermost.rnbeta;', + new_string: 'package com.mattermost.rn;' + ) + + find_replace_string( + path_to_file: './android/app/src/main/java/com/mattermost/rn/NotificationReplyService.java', old_string: 'package com.mattermost.rnbeta;', new_string: 'package com.mattermost.rn;' ) diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index fa042cd0c..024e38532 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -5,7 +5,6 @@ }; objectVersion = 46; objects = { - /* Begin PBXBuildFile section */ 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; @@ -1435,7 +1434,6 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/$(TARGET_NAME)\"", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1453,7 +1451,6 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LIBRARY_SEARCH_PATHS = ( "$(inherited)", - "\"$(SRCROOT)/$(TARGET_NAME)\"", ); PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index 0dfe7074b..b73499115 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -60,4 +60,16 @@ { [RNNotifications didReceiveLocalNotification:notification]; } + +// Required for the notification actions. +- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)())completionHandler +{ + [RNNotifications handleActionWithIdentifier:identifier forLocalNotification:notification withResponseInfo:responseInfo completionHandler:completionHandler]; +} + +- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo withResponseInfo:(NSDictionary *)responseInfo completionHandler:(void (^)())completionHandler +{ + [RNNotifications handleActionWithIdentifier:identifier forRemoteNotification:userInfo withResponseInfo:responseInfo completionHandler:completionHandler]; +} + @end diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 13f33d3af..e326a3199 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -44,7 +44,7 @@ NSCameraUsageDescription Take a Photo or Video and upload it to your mattermost instance NSLocationWhenInUseUsageDescription - + NSPhotoLibraryUsageDescription Upload Photos and Videos to your Mattermost instance UIAppFonts @@ -57,16 +57,8 @@ MaterialIcons.ttf Octicons.ttf Zocial.ttf - Entypo.ttf - EvilIcons.ttf - FontAwesome.ttf - Foundation.ttf - Ionicons.ttf MaterialCommunityIcons.ttf - MaterialIcons.ttf - Octicons.ttf SimpleLineIcons.ttf - Zocial.ttf OpenSans-Bold.ttf OpenSans-BoldItalic.ttf OpenSans-ExtraBold.ttf diff --git a/yarn.lock b/yarn.lock index ebc7873cf..2c8c0442e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4560,8 +4560,8 @@ react-native-navigation@1.1.131: lodash "4.x.x" react-native-notifications@enahum/react-native-notifications.git: - version "1.1.11" - resolved "https://codeload.github.com/enahum/react-native-notifications/tar.gz/a8450bcbfc6ae46ed4b97f6fe723aac44c8ad598" + version "1.1.15" + resolved "https://codeload.github.com/enahum/react-native-notifications/tar.gz/084d5e928d8149e711c1b6c384f3d87cc16072f9" dependencies: core-js "^1.0.0" uuid "^2.0.3"