diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index d89194b6ce..33ae65cbfa 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -71,7 +71,6 @@ - persons; - private HashMap chats; - private Context context; - private Intent serviceIntent; - private Boolean shouldRefreshNotifications; - private int ONE_TO_ONE_CHAT_TYPE = 1; - private int PRIVATE_GROUP_CHAT_TYPE = 3; - - //NOTE: we use a dynamically created BroadcastReceiver here so that we can capture - //intents from notifications and act on them. For instance when tapping/dismissing - //a chat notification we want to clear the chat so that next messages don't show - //the messages that we have seen again - private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction() == ACTION_TAP_NOTIFICATION || - intent.getAction() == ACTION_DELETE_NOTIFICATION) { - String chatId = intent.getExtras().getString("im.status.ethereum.chatId"); - int chatType = intent.getExtras().getInt("im.status.ethereum.chatType"); - if (intent.getAction() == ACTION_TAP_NOTIFICATION) { - context.startActivity(getOpenAppIntent(chatId, chatType)); - } - removeChat(chatId); - // clean up the group notifications when there is no - // more unread chats - if (chats.size() == 0) { - notificationManager.cancelAll(); - }} - if (intent.getAction() == ACTION_TAP_STOP) { - stop(); - System.exit(0); - } - Log.e(TAG, "intent received: " + intent.getAction()); - } - }; - - private void registerBroadcastReceiver() { - IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_DELETE_NOTIFICATION); - filter.addAction(ACTION_TAP_NOTIFICATION); - filter.addAction(ACTION_TAP_STOP); - context.registerReceiver(notificationActionReceiver, filter); - Log.e(TAG, "Broadcast Receiver registered"); - } - - public Intent getOpenAppIntent() { - Class intentClass; - String packageName = context.getPackageName(); - Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); - String className = launchIntent.getComponent().getClassName(); - try { - intentClass = Class.forName(className); - } catch (ClassNotFoundException e) { - e.printStackTrace(); - return null; - } - Intent intent = new Intent(context, intentClass); - intent.addCategory(Intent.CATEGORY_BROWSABLE); - intent.setAction(Intent.ACTION_VIEW); - //NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well, - //the answer is a simple as it can get in the Android native development world. I noticed - //that my initial setup was opening the app but wasn't triggering any events on the js side, like - //the links do from the browser. So I compared both intents and noticed that the link from - //the browser produces an intent with the flag 0x14000000. I found out that it was the following - //flags in this link: - //https://stackoverflow.com/questions/52390129/android-intent-setflags-issue - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); - return intent; - } - - //NOTE: this method takes a chatId and returns an intent that will open the app in that chat - //Once we support other kind of notifications we will need to adapt it. The simplest method - //is probably to pass the universal link as param instead of the chatId. - public Intent getOpenAppIntent(String chatId, int chatType) { - Intent intent = getOpenAppIntent(); - String path = ""; - if (chatType == ONE_TO_ONE_CHAT_TYPE) { - path = "p/"; - } else if (chatType == PRIVATE_GROUP_CHAT_TYPE) { - path = "g/args?a2="; - } - intent.setData(Uri.parse("status-im://" + path + chatId)); - return intent; - } - - public NewMessageSignalHandler(Context context) { - // NOTE: when instanciated the NewMessageSignalHandler class starts a foreground service - // to keep the app running in the background in order to receive notifications - // call the stop() method in order to stop the service - this.context = context; - this.persons = new HashMap(); - this.chats = new HashMap(); - this.notificationManager = context.getSystemService(NotificationManager.class); - this.createNotificationChannel(); - this.shouldRefreshNotifications = false; - Log.e(TAG, "Starting Foreground Service"); - Intent serviceIntent = new Intent(context, ForegroundService.class); - context.startService(serviceIntent); - this.registerBroadcastReceiver(); - } - - public void stop() { - Log.e(TAG, "Stopping Foreground Service"); - //NOTE: we cancel all the current notifications, because the intents can't be used anymore - //since the broadcast receiver will be killed as well and won't be able to handle any intent - notificationManager.cancelAll(); - Intent serviceIntent = new Intent(context, ForegroundService.class); - context.stopService(serviceIntent); - context.unregisterReceiver(notificationActionReceiver); - } - - private void createNotificationChannel() { - // Create the NotificationChannel, but only on API 26+ because - // the NotificationChannel class is new and not in the support library - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Uri soundUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + "/" + R.raw.notification_sound); - NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH); - channel.setDescription(context.getResources().getString(R.string.channel_description)); - AudioAttributes audioAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION) - .build(); - channel.setSound(soundUri, audioAttributes); - channel.setShowBadge(true); - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - } - - private void removeChat(String chatId) { - this.chats.remove(chatId); - } - - private PendingIntent createOnDismissedIntent(Context context, int notificationId, String chatId, int chatType) { - Intent intent = new Intent(ACTION_DELETE_NOTIFICATION); - intent.putExtra("im.status.ethereum.chatId", chatId); - intent.putExtra("im.status.ethereum.chatType", chatType); - return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); - } - - private PendingIntent createOnTapIntent(Context context, int notificationId, String chatId, int chatType) { - Intent intent = new Intent(ACTION_TAP_NOTIFICATION); - intent.putExtra("im.status.ethereum.chatId", chatId); - intent.putExtra("im.status.ethereum.chatType", chatType); - return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); - } - - public void notify(int notificationId, StatusChat chat) { - NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me"); - ArrayList messages = chat.getMessages(); - for (int i = 0; i < messages.size(); i++) { - StatusMessage message = messages.get(i); - messagingStyle.addMessage(message.getText(), - message.getTimestamp(), - message.getAuthor()); - } - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_stat_notify_status) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setStyle(messagingStyle) - .setGroup(GROUP_STATUS_MESSAGES) - .setGroupSummary(true) - .setContentIntent(createOnTapIntent(context, notificationId, chat.getId(), chat.getType())) - .setDeleteIntent(createOnDismissedIntent(context, notificationId, chat.getId(), chat.getType())) - .setNumber(messages.size()) - .setAutoCancel(true); - if (Build.VERSION.SDK_INT >= 21) { - builder.setVibrate(new long[0]); - } - notificationManager.notify(notificationId, builder.build()); - } - - public void refreshNotifications() { - NotificationCompat.InboxStyle summaryStyle = new NotificationCompat.InboxStyle(); - int notificationId = 2; // we start at 2 because the service is using 1 and can't use 0 - Iterator chatIterator = chats.values().iterator(); - while(chatIterator.hasNext()) { - StatusChat chat = (StatusChat)chatIterator.next(); - notify(notificationId, chat); - notificationId++; - } - } - - void handleNewMessage (Bundle data) { - upsertChat(data); - upsertMessage(data); - - if(shouldRefreshNotifications) { - refreshNotifications(); - shouldRefreshNotifications = false; - } - } - - private Person getPerson(String publicKey, String icon, String name) { - // TODO: invalidate cache if icon and name are not the same as - // the Person returned (in case the user set a different icon or username for instance) - // not critical it's just for notifications at the moment - // using a HashMap to cache Person because it's immutable - Person person = persons.get(publicKey); - if (person == null) { - String base64Image = icon.split(",")[1]; - byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT); - Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length); - person = new Person.Builder().setIcon(IconCompat.createWithBitmap(decodedByte)).setName(name).build(); - persons.put(publicKey, person); - } - return person; - } - - private void upsertChat(Bundle data) { - String id = data.getString("chatId"); - int type = Integer.parseInt(data.getString("chatType")); - StatusChat chat = chats.get(id); - - // if the chat was not already there, we create one - if (chat == null) { - chat = new StatusChat(id, type); - } - - chats.put(id, chat); - } - - private void upsertMessage(Bundle data) { - String chatId = data.getString("chatId"); - StatusChat chat = chats.get(chatId); - if (chat == null) { - return; - } - - StatusMessage message = createMessage(data); - if (message != null) { - chat.appendMessage(message); - chats.put(chatId, chat); - shouldRefreshNotifications = true; - } - } - - private StatusMessage createMessage(Bundle data) { - Person author = getPerson(data.getString("from"), data.getString("identicon"), data.getString("alias")); - return new StatusMessage(author, data.getLong("whisperTimestamp"), data.getString("text")); - } -} - -class StatusChat { - private ArrayList messages; - private String id; - private String name; - private int type; - - StatusChat(String id, int type) { - this.id = id; - this.type = type; - this.messages = new ArrayList(); - this.name = name; - } - - public String getId() { - return id; - } - - public int getType() { - return this.type; - } - - public String getName() { - - //TODO this should be improved as it would rename the chat - // after our own user if we were posting from another device - // in 1-1 chats it should be the name of the user whose - // key is different than ours - StatusMessage message = getLastMessage(); - if (message == null) { - return "no-name"; - } - return message.getAuthor().getName().toString(); - } - - private StatusMessage getLastMessage() { - if (messages.size() > 0) { - return messages.get(messages.size()-1); - } - return null; - } - - public long getTimestamp() { - return getLastMessage().getTimestamp(); - } - - public ArrayList getMessages() { - return messages; - } - - public void appendMessage(StatusMessage message) { - this.messages.add(message); - } - - public String getSummary() { - return "" + getLastMessage().getAuthor().getName() + ": " + getLastMessage().getText(); - } -} - - -class StatusMessage { - public Person getAuthor() { - return author; - } - - public long getTimestamp() { - return timestamp; - } - - public String getText() { - return text; - } - - private Person author; - private long timestamp; - private String text; - - StatusMessage(Person author, long timestamp, String text) { - this.author = author; - this.timestamp = timestamp; - this.text = text; - } -} diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java index 4ccd38f394..ce7a2e71b8 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotification.java @@ -37,7 +37,7 @@ public class PushNotification extends ReactContextBaseJavaModule implements Acti private PushNotificationHelper pushNotificationHelper; private PushNotificationJsDelivery delivery; private ReactApplicationContext reactContext; - private NewMessageSignalHandler newMessageSignalHandler; + private boolean started; public PushNotification(ReactApplicationContext reactContext) { super(reactContext); @@ -104,33 +104,30 @@ public class PushNotification extends ReactContextBaseJavaModule implements Acti @ReactMethod public void presentLocalNotification(ReadableMap details) { + if (!this.started) { + return; + } + Bundle bundle = Arguments.toBundle(details); // If notification ID is not provided by the user, generate one at random if (bundle.getString("id") == null) { bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt())); } - String type = bundle.getString("type"); - if (type != null && type.equals("message")) { - if (this.newMessageSignalHandler != null) { - newMessageSignalHandler.handleNewMessage(bundle); - } - } else { - pushNotificationHelper.sendToNotificationCentre(bundle); - } + pushNotificationHelper.sendToNotificationCentre(bundle); } @ReactMethod public void enableNotifications() { - this.newMessageSignalHandler = new NewMessageSignalHandler(reactContext); + this.started = true; + this.pushNotificationHelper.start(); } @ReactMethod public void disableNotifications() { - if (newMessageSignalHandler != null) { - newMessageSignalHandler.stop(); - newMessageSignalHandler = null; - } + if (this.started) { + this.started = false; + this.pushNotificationHelper.stop(); + } } - } diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java index 9f7f94697f..91490b5f1c 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/pushnotifications/PushNotificationHelper.java @@ -11,6 +11,8 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; +import android.content.BroadcastReceiver; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.res.Resources; @@ -24,9 +26,12 @@ import android.os.Build; import android.os.Bundle; import android.service.notification.StatusBarNotification; import android.util.Log; +import android.util.Base64; import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; +import androidx.core.app.Person; +import androidx.core.graphics.drawable.IconCompat; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReadableArray; @@ -43,8 +48,10 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; +import java.util.HashMap; import java.util.Map; +import im.status.ethereum.module.R; import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG; public class PushNotificationHelper { @@ -53,11 +60,91 @@ public class PushNotificationHelper { private static final long DEFAULT_VIBRATION = 300L; private static final String CHANNEL_ID = "status-im-notifications"; + public static final String ACTION_DELETE_NOTIFICATION = "im.status.ethereum.module.DELETE_NOTIFICATION"; + public static final String ACTION_TAP_NOTIFICATION = "im.status.ethereum.module.TAP_NOTIFICATION"; + public static final String ACTION_TAP_STOP = "im.status.ethereum.module.TAP_STOP"; + + private NotificationManager notificationManager; + + + private HashMap persons; + private HashMap messageGroups; public PushNotificationHelper(Application context) { this.context = context; + this.persons = new HashMap(); + this.messageGroups = new HashMap(); + this.notificationManager = context.getSystemService(NotificationManager.class); + this.registerBroadcastReceiver(); } + public Intent getOpenAppIntent(String deepLink) { + Class intentClass; + String packageName = context.getPackageName(); + Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName); + String className = launchIntent.getComponent().getClassName(); + try { + intentClass = Class.forName(className); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + return null; + } + Intent intent = new Intent(context, intentClass); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + intent.setAction(Intent.ACTION_VIEW); + //NOTE: you might wonder, why the heck did he decide to set these flags in particular. Well, + //the answer is a simple as it can get in the Android native development world. I noticed + //that my initial setup was opening the app but wasn't triggering any events on the js side, like + //the links do from the browser. So I compared both intents and noticed that the link from + //the browser produces an intent with the flag 0x14000000. I found out that it was the following + //flags in this link: + //https://stackoverflow.com/questions/52390129/android-intent-setflags-issue + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.setData(Uri.parse(deepLink)); + return intent; + } + + //NOTE: we use a dynamically created BroadcastReceiver here so that we can capture + //intents from notifications and act on them. For instance when tapping/dismissing + //a chat notification we want to clear the chat so that next messages don't show + //the messages that we have seen again + private final BroadcastReceiver notificationActionReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == ACTION_TAP_NOTIFICATION || + intent.getAction() == ACTION_DELETE_NOTIFICATION) { + String deepLink = intent.getExtras().getString("im.status.ethereum.deepLink"); + String groupId = intent.getExtras().getString("im.status.ethereum.groupId"); + if (intent.getAction() == ACTION_TAP_NOTIFICATION) { + context.startActivity(getOpenAppIntent(deepLink)); + } + if (groupId != null) { + removeGroup(groupId); + // clean up the group notifications when there is no + // more unread chats + if (messageGroups.size() == 0) { + notificationManager.cancelAll(); + }} + } + if (intent.getAction() == ACTION_TAP_STOP) { + stop(); + System.exit(0); + } + Log.e(LOG_TAG, "intent received: " + intent.getAction()); + } + }; + + private void registerBroadcastReceiver() { + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_DELETE_NOTIFICATION); + filter.addAction(ACTION_TAP_NOTIFICATION); + filter.addAction(ACTION_TAP_STOP); + context.registerReceiver(notificationActionReceiver, filter); + Log.e(LOG_TAG, "Broadcast Receiver registered"); + } + + + private NotificationManager notificationManager() { return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); } @@ -95,7 +182,12 @@ public class PushNotificationHelper { aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl")); } + public void handleConversation(final Bundle bundle) { + this.addStatusMessage(bundle); + } + public void sendToNotificationCentreWithPicture(final Bundle bundle, Bitmap largeIconBitmap, Bitmap bigPictureBitmap) { + try { Class intentClass = getMainActivityClass(); if (intentClass == null) { @@ -103,6 +195,11 @@ public class PushNotificationHelper { return; } + if (bundle.getBoolean("isConversation")) { + this.handleConversation(bundle); + return; + } + if (bundle.getString("message") == null) { // this happens when a 'data' notification is received - we do not synthesize a local notification in this case Log.d(LOG_TAG, "Ignore this message if you sent data-only notification. Cannot send to notification centre because there is no 'message' field in: " + bundle); @@ -325,10 +422,10 @@ public class PushNotificationHelper { } } - int notificationID = Integer.parseInt(notificationIdString); + int notificationID = notificationIdString.hashCode(); - PendingIntent pendingIntent = PendingIntent.getActivity(context, notificationID, intent, - PendingIntent.FLAG_UPDATE_CURRENT); + notification.setContentIntent(createOnTapIntent(context, notificationID, bundle.getString("deepLink"))) + .setDeleteIntent(createOnDismissedIntent(context, notificationID, bundle.getString("deepLink"))); NotificationManager notificationManager = notificationManager(); @@ -367,7 +464,6 @@ public class PushNotificationHelper { notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false)); notification.setChannelId(channel_id); - notification.setContentIntent(pendingIntent); JSONArray actionsArray = null; try { @@ -546,4 +642,155 @@ public class PushNotificationHelper { return false; } + private Person getPerson(Bundle bundle) { + String base64Image = bundle.getString("icon").split(",")[1]; + byte[] decodedString = Base64.decode(base64Image, Base64.DEFAULT); + Bitmap decodedByte = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length); + + String name = bundle.getString("name"); + + return new Person.Builder().setIcon(IconCompat.createWithBitmap(decodedByte)).setName(name).build(); + } + + private StatusMessage createMessage(Bundle data) { + Person author = getPerson(data.getBundle("notificationAuthor")); + return new StatusMessage(author, data.getLong("timestamp"), data.getString("message")); + } + + private PendingIntent createGroupOnDismissedIntent(Context context, int notificationId, String groupId, String deepLink) { + Intent intent = new Intent(ACTION_DELETE_NOTIFICATION); + intent.putExtra("im.status.ethereum.deepLink", deepLink); + intent.putExtra("im.status.ethereum.groupId", groupId); + return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private PendingIntent createGroupOnTapIntent(Context context, int notificationId, String groupId, String deepLink) { + Intent intent = new Intent(ACTION_TAP_NOTIFICATION); + intent.putExtra("im.status.ethereum.deepLink", deepLink); + intent.putExtra("im.status.ethereum.groupId", groupId); + return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private PendingIntent createOnTapIntent(Context context, int notificationId, String deepLink) { + Intent intent = new Intent(ACTION_TAP_NOTIFICATION); + intent.putExtra("im.status.ethereum.deepLink", deepLink); + return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private PendingIntent createOnDismissedIntent(Context context, int notificationId, String deepLink) { + Intent intent = new Intent(ACTION_DELETE_NOTIFICATION); + intent.putExtra("im.status.ethereum.deepLink", deepLink); + return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + + public void addStatusMessage(Bundle bundle) { + String conversationId = bundle.getString("conversationId"); + StatusMessageGroup group = this.messageGroups.get(conversationId); + NotificationManager notificationManager = notificationManager(); + + if (group == null) { + group = new StatusMessageGroup(conversationId); + } + + this.messageGroups.put(conversationId, group); + + group.addMessage(createMessage(bundle)); + + NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me"); + ArrayList messages = group.getMessages(); + for (int i = 0; i < messages.size(); i++) { + StatusMessage message = messages.get(i); + messagingStyle.addMessage(message.getText(), + message.getTimestamp(), + message.getAuthor()); + } + + if (bundle.getString("title") != null) { + messagingStyle.setConversationTitle(bundle.getString("title")); + } + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_stat_notify_status) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setCategory(NotificationCompat.CATEGORY_MESSAGE) + .setStyle(messagingStyle) + .setGroup(conversationId) + .setOnlyAlertOnce(true) + .setGroupSummary(true) + .setContentIntent(createGroupOnTapIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink"))) + .setDeleteIntent(createGroupOnDismissedIntent(context, conversationId.hashCode(), conversationId, bundle.getString("deepLink"))) + .setNumber(messages.size() + 1) + .setAutoCancel(true); + if (Build.VERSION.SDK_INT >= 21) { + builder.setVibrate(new long[0]); + } + notificationManager.notify(conversationId.hashCode(), builder.build()); + } + + class StatusMessageGroup { + private ArrayList messages; + private String id; + + StatusMessageGroup(String id) { + this.id = id; + this.messages = new ArrayList(); + } + public ArrayList getMessages() { + return messages; + } + + public void addMessage(StatusMessage message) { + this.messages.add(message); + } + + public String getId() { + return this.id; + } + } + + class StatusMessage { + public Person getAuthor() { + return author; + } + + public long getTimestamp() { + return timestamp; + } + + public String getText() { + return text; + } + + private Person author; + private long timestamp; + private String text; + + StatusMessage(Person author, long timestamp, String text) { + this.author = author; + this.timestamp = timestamp; + this.text = text; + } + } + + private void removeGroup(String groupId) { + this.messageGroups.remove(groupId); + } + + public void start() { + Log.e(LOG_TAG, "Starting Foreground Service"); + Intent serviceIntent = new Intent(context, ForegroundService.class); + context.startService(serviceIntent); + this.registerBroadcastReceiver(); + } + + public void stop() { + Log.e(LOG_TAG, "Stopping Foreground Service"); + //NOTE: we cancel all the current notifications, because the intents can't be used anymore + //since the broadcast receiver will be killed as well and won't be able to handle any intent + notificationManager.cancelAll(); + Intent serviceIntent = new Intent(context, ForegroundService.class); + context.stopService(serviceIntent); + context.unregisterReceiver(notificationActionReceiver); + } } diff --git a/src/status_im/core.cljs b/src/status_im/core.cljs index a57a271db7..f3c6698317 100644 --- a/src/status_im/core.cljs +++ b/src/status_im/core.cljs @@ -30,8 +30,6 @@ (status/set-soft-input-mode status/adjust-resize)) (.registerComponent ^js (.-AppRegistry rn) "StatusIm" #(reagent/reactify-component views/root)) (notifications/listen-notifications) - (when platform/android? - (.registerHeadlessTask ^js (.-AppRegistry rn) "LocalNotifications" notifications/handle)) (snoopy/subscribe!) (when (and js/goog.DEBUG platform/ios? DevSettings) ;;on Android this method doesn't work diff --git a/src/status_im/notifications/local.cljs b/src/status_im/notifications/local.cljs index 34efdaddce..cd6a80a22e 100644 --- a/src/status_im/notifications/local.cljs +++ b/src/status_im/notifications/local.cljs @@ -1,6 +1,5 @@ (ns status-im.notifications.local - (:require [taoensso.timbre :as log] - [status-im.utils.fx :as fx] + (:require [status-im.utils.fx :as fx] [status-im.ethereum.decode :as decode] ["@react-native-community/push-notification-ios" :default pn-ios] [status-im.notifications.android :as pn-android] @@ -14,11 +13,8 @@ [re-frame.core :as re-frame] [status-im.ui.components.react :as react] [cljs-bean.core :as bean] - [status-im.ui.screens.chat.components.reply :as reply] [clojure.string :as clojure.string] - [status-im.chat.models :as chat.models] - [status-im.constants :as constants] - [status-im.utils.identicon :as identicon])) + [status-im.chat.models :as chat.models])) (def default-erc20-token {:symbol :ERC20 @@ -39,21 +35,8 @@ {:notificationType "local-notification"}))}))) (defn local-push-android - [{:keys [title message icon user-info channel-id type] - :as notification - :or {channel-id "status-im-notifications"}}] - (when notification - (pn-android/present-local-notification - (merge {:channelId channel-id - :title title - :message message - :showBadge false} - (when user-info - {:userInfo (bean/->js user-info)}) - (when icon - {:largeIconUrl (:uri (react/resolve-asset-source icon))}) - (when (= type "message") - notification))))) + [notification] + (pn-android/present-local-notification notification)) (defn handle-notification-press [{{deep-link :deepLink} :userInfo interaction :userInteraction}] @@ -106,65 +89,16 @@ nil)] {:title title :icon (get-in token [:icon :source]) + :deepLink (:deepLink notification) :user-info notification :message description})) -(defn chat-by-message - [{:keys [chats]} {:keys [localChatId from]}] - (if-let [chat (get chats localChatId)] - (assoc chat :chat-id localChatId) - (assoc (get chats from) :chat-id from))) - (defn show-message-pn? [{{:keys [app-state]} :db :as cofx} - {{:keys [chat-id]} :chat}] - (or (= app-state "background") - (not (chat.models/foreground-chat? cofx chat-id)))) - -(defn create-message-notification - ([{:keys [db] :as cofx} {{:keys [message]} :body :as notification}] - (when-not (nil? cofx) - (let [chat (chat-by-message db message) - contact-id (get message :from) - contact (get-in db [:contacts/contacts contact-id]) - notification (assoc notification - :chat chat - :contact-id contact-id - :contact contact)] - (when (show-message-pn? cofx notification) - (create-message-notification notification))))) - ([{{:keys [message]} :body - {:keys [chat-type chat-id] :as chat} :chat - {:keys [identicon]} :contact - contact-id :contact-id}] - (when (and chat-type chat-id) - ;;TODO : DON'T USE SUBS IN EVENTS - (let [contact-name @(re-frame/subscribe - [:contacts/contact-name-by-identity contact-id]) - group-chat? (not= chat-type constants/one-to-one-chat-type) - title (clojure.string/join - " " - (cond-> [contact-name] - group-chat? - (conj - ;; TODO(rasom): to be translated - "in") - - group-chat? - (conj - (str (when (contains? #{constants/public-chat-type - constants/community-chat-type} - chat-type) - "#") - (get chat :name)))))] - {:type "message" - :chatType (str chat-type) - :from title - :chatId chat-id - :alias title - :identicon (or identicon (identicon/identicon contact-id)) - :whisperTimestamp (get message :whisperTimestamp) - :text (reply/get-quoted-text-with-mentions (:parsedText message))})))) + notification] + (let [chat-id (get-in notification [:body :chat :id])] + (or (= app-state "background") + (not (chat.models/foreground-chat? cofx chat-id))))) (defn create-notification ([notification] @@ -172,7 +106,7 @@ ([cofx {:keys [bodyType] :as notification}] (assoc (case bodyType - "message" (create-message-notification cofx notification) + "message" (when (show-message-pn? cofx notification) notification) "transaction" (create-transfer-notification notification) nil) :body-type bodyType))) @@ -194,16 +128,3 @@ (if platform/ios? {::local-push-ios evt} (local-notification-android cofx evt))) - -(defn handle [] - (fn [^js message] - (let [evt (types/json->clj (.-event message))] - (js/Promise. - (fn [on-success on-error] - (try - (when (= "local-notifications" (:type evt)) - (re-frame/dispatch [::local-notification-android (:event evt)])) - (on-success) - (catch :default e - (log/warn "failed to handle background notification" e) - (on-error e)))))))) diff --git a/src/status_im/router/core.cljs b/src/status_im/router/core.cljs index 3d257a4392..9e8aab1ae6 100644 --- a/src/status_im/router/core.cljs +++ b/src/status_im/router/core.cljs @@ -43,6 +43,7 @@ "b/" browser-extractor "browser/" browser-extractor ["p/" :chat-id] :private-chat + ["cr/" :community-id] :community-requests "g/" group-chat-extractor ["wallet/" :account] :wallet-account ["u/" :user-id] :user @@ -201,6 +202,9 @@ (spec/valid? :global/public-key uri) (match-contact-async chain {:user-id uri} cb) + (= handler :community-requests) + (cb {:type handler :community-id (:community-id route-params)}) + (= handler :referrals) (cb (match-referral route-params)) diff --git a/src/status_im/utils/universal_links/core.cljs b/src/status_im/utils/universal_links/core.cljs index 9dde2a6285..c652209b1a 100644 --- a/src/status_im/utils/universal_links/core.cljs +++ b/src/status_im/utils/universal_links/core.cljs @@ -24,11 +24,12 @@ (def domains {:external "https://join.status.im" :internal "status-im:/"}) -(def links {:public-chat "%s/%s" - :private-chat "%s/p/%s" - :group-chat "%s/g/%s" - :user "%s/u/%s" - :browse "%s/b/%s"}) +(def links {:public-chat "%s/%s" + :private-chat "%s/p/%s" + :community-requests "%s/cr/%s" + :group-chat "%s/g/%s" + :user "%s/u/%s" + :browse "%s/b/%s"}) (defn generate-link [link-type domain-type param] (gstring/format (get links link-type) @@ -59,6 +60,10 @@ {:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code) :content (i18n/label :t/can-not-add-yourself)}}))) +(fx/defn handle-community-requests [cofx {:keys [community-id]}] + (log/info "universal-links: handling community request " community-id) + (navigation/navigate-to-cofx cofx :community-requests-to-join {:community-id community-id})) + (fx/defn handle-public-chat [cofx {:keys [topic]}] (log/info "universal-links: handling public chat" topic) (when (seq topic) @@ -113,20 +118,22 @@ {:events [::match-value]} [cofx url {:keys [type] :as data}] (case type - :group-chat (handle-group-chat cofx data) - :public-chat (handle-public-chat cofx data) - :private-chat (handle-private-chat cofx data) - :contact (handle-view-profile cofx data) - :browser (handle-browse cofx data) - :eip681 (handle-eip681 cofx data) - :referrals (handle-referrer-url cofx data) - :wallet-account (handle-wallet-account cofx data) + :group-chat (handle-group-chat cofx data) + :public-chat (handle-public-chat cofx data) + :private-chat (handle-private-chat cofx data) + :community-requests (handle-community-requests cofx data) + :contact (handle-view-profile cofx data) + :browser (handle-browse cofx data) + :eip681 (handle-eip681 cofx data) + :referrals (handle-referrer-url cofx data) + :wallet-account (handle-wallet-account cofx data) (handle-not-found url))) (fx/defn route-url "Match a url against a list of routes and handle accordingly" [{:keys [db]} url] {::router/handle-uri {:chain (ethereum/chain-keyword db) + :chats (:chats db) :uri url :cb #(re-frame/dispatch [::match-value url %])}}) diff --git a/status-go-version.json b/status-go-version.json index 18a8b9e41d..ce8b4da197 100644 --- a/status-go-version.json +++ b/status-go-version.json @@ -2,7 +2,7 @@ "_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh ' instead", "owner": "status-im", "repo": "status-go", - "version": "v0.74.1", - "commit-sha1": "5a76e93063e3b9ef2ded83a5f545f7149401e8f0", - "src-sha256": "0581048fvqg0fxv03pm9a0drfiq32slxliwamnch3052avpdkycj" + "version": "v0.74.2", + "commit-sha1": "1724ecffa178e9bcb04ae6d9d95d276c02878b6f", + "src-sha256": "1kn6996brpva1bdj8bv1mscwx17hynqk8xb667ai6qpdriscr22d" } diff --git a/test/appium/tests/atomic/account_management/test_profile.py b/test/appium/tests/atomic/account_management/test_profile.py index b337af15aa..9518f402a4 100644 --- a/test/appium/tests/atomic/account_management/test_profile.py +++ b/test/appium/tests/atomic/account_management/test_profile.py @@ -1123,7 +1123,7 @@ class TestProfileMultipleDevice(MultipleDeviceTestCase): home_1.just_fyi('check that PN is received and after tap you are redirected to public chat') home_1.open_notification_bar() - home_1.element_by_text_part('%s in #%s' % (username_2, chat_name)).click() + home_1.element_by_text_part(username_2).click() chat_1.element_starts_with_text(user_1['ens'] +'.stateofus.eth','button').click() if not profile_1.contacts_button.is_element_displayed(): self.errors.append('Was not redirected to own profile after tapping on mention of myself from another user!') diff --git a/test/appium/tests/atomic/chats/test_group_chat.py b/test/appium/tests/atomic/chats/test_group_chat.py index 5154671ed9..cb688d747c 100644 --- a/test/appium/tests/atomic/chats/test_group_chat.py +++ b/test/appium/tests/atomic/chats/test_group_chat.py @@ -57,6 +57,7 @@ class TestGroupChatMultipleDevice(MultipleDeviceTestCase): for chat in (device_1_chat, device_2_chat): if not chat.chat_element_by_text(join_system_message).is_element_displayed(): self.errors.append('System message after joining group chat is not shown') + device_2_chat.home_button.click(desired_view="home") message_1 = "Message from device: %s" % device_1_chat.driver.number device_1_chat.send_message(message_1) if device_1_chat.chat_element_by_text(message_1).status != 'delivered': diff --git a/test/appium/tests/atomic/chats/test_one_to_one.py b/test/appium/tests/atomic/chats/test_one_to_one.py index 46665e6876..265a012ad2 100644 --- a/test/appium/tests/atomic/chats/test_one_to_one.py +++ b/test/appium/tests/atomic/chats/test_one_to_one.py @@ -360,7 +360,7 @@ class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase): chat_1.send_message_button.click() device_2.open_notification_bar() - chat_2 = home_2.click_upon_push_notification_by_text("audio message") + chat_2 = home_2.click_upon_push_notification_by_text("Audio") listen_time = 5