Add community notifications on android & IOS

[Fixes: #11806]
[Fixes: #11877]

Features added

An admin should receive a notification on android if a community request is made
Clicking on the push notification will take you to the list of requests
Changes to push notifications
Push notification for messages are grouped by chat, similarly to discord. Only the first notification in a chat will alert, the next will be "silent" notification. Meaning you will see the message but it will not alert. This is consistent with discord/whatsapp.
Clicking on a transaction notification will take you to wallet (not sure it was doing that before, but it might have)
Changed the behavior of the notification toggle, before Notifications and wallet transactions were separate, meaning you could have wallet transaction and Notifications disabled and you'd still receive transactions notifications. Now you need to have Notifications enabled to receive wallet transactions. Eventually we will have an option to toggle message notifications.

Technical details

Removes headless tasks, they were not used
Message notifications are passed unchanged to java, we still check if the chat is in the foreground, but no modifications are made to the notification object. This should get us closer to avoid clojure completely.
Merged the two notifications implementation (NewMessageSignalHandler and PushNotificationHelper). We should split maybe off in more meaningful classes, but there's less code duplication now, and it can be re-used for non-chat (communities) notifications.
Parsing of text for message notifications is done in status-go
Signal is not passed to status-react if notifications are not enabled

Next step

To completely remove notification code from status-react the following are required:

Java needs to be alerted of which chat is in the foreground and whether the app is in the foreground
Transaction notification body message need to be created in status-go
Notification signal needs to be stopped in Java

Limitations

If the name of a contact changes, the notification will not change, once is displayed, it won't be modified
This commit is contained in:
Andrea Maria Piana 2021-03-04 10:27:55 +01:00
parent 460685212a
commit 116b4d8812
No known key found for this signature in database
GPG Key ID: AA6CCA6DE0E06424
14 changed files with 304 additions and 531 deletions

View File

@ -71,7 +71,6 @@
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
<service android:name="im.status.ethereum.pushnotifications.ForegroundService"></service>
<service android:name="im.status.ethereum.module.LocalNotificationsService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"

View File

@ -1,20 +0,0 @@
package im.status.ethereum.module;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.HeadlessJsTaskService;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.jstasks.HeadlessJsTaskConfig;
import javax.annotation.Nullable;
public class LocalNotificationsService extends HeadlessJsTaskService {
@Override
protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
return new HeadlessJsTaskConfig("LocalNotifications", Arguments.fromBundle(extras), 60000, true);
}
return null;
}
}

View File

@ -55,7 +55,7 @@ public class ForegroundService extends Service {
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
Intent stopIntent = new Intent(NewMessageSignalHandler.ACTION_TAP_STOP);
Intent stopIntent = new Intent(PushNotificationHelper.ACTION_TAP_STOP);
PendingIntent stopPendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT);
String content = context.getResources().getString(R.string.keep_status_running);

View File

@ -1,381 +0,0 @@
package im.status.ethereum.pushnotifications;
import android.content.Context;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.BroadcastReceiver;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import androidx.core.app.Person;
import androidx.core.app.Person.Builder;
import android.util.Base64;
import androidx.core.graphics.drawable.IconCompat;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.NotificationCompat;
import android.os.Build;
import android.os.Bundle;
import android.net.Uri;
import android.media.AudioAttributes;
import android.util.Log;
import im.status.ethereum.module.R;
public class NewMessageSignalHandler {
//NOTE: currently we only show notifications for 1-1 chats, in the future we
//will most likely extend to other kind of notifications. The first step will
//be to define actions for these notifications, add it to the filter in
//`registerBroadcastReceiver` method, and add some action specific code
//in notificationActionReceiver.
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 static final String GROUP_STATUS_MESSAGES = "im.status.notifications.message";
private static final String CHANNEL_NAME = "Status";
private static final String CHANNEL_ID = "status-chat-notifications";
private static final String TAG = "StatusModule";
private NotificationManager notificationManager;
private HashMap<String, Person> persons;
private HashMap<String, StatusChat> 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<String, Person>();
this.chats = new HashMap<String, StatusChat>();
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<StatusMessage> 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<StatusChat> 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<StatusMessage> 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<StatusMessage>();
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<StatusMessage> getMessages() {
return messages;
}
public void appendMessage(StatusMessage message) {
this.messages.add(message);
}
public String getSummary() {
return "<b>" + getLastMessage().getAuthor().getName() + "</b>: " + 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;
}
}

View File

@ -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();
}
}
}

View File

@ -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<String, Person> persons;
private HashMap<String, StatusMessageGroup> messageGroups;
public PushNotificationHelper(Application context) {
this.context = context;
this.persons = new HashMap<String, Person>();
this.messageGroups = new HashMap<String, StatusMessageGroup>();
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<StatusMessage> 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<StatusMessage> messages;
private String id;
StatusMessageGroup(String id) {
this.id = id;
this.messages = new ArrayList<StatusMessage>();
}
public ArrayList<StatusMessage> 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);
}
}

View File

@ -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

View File

@ -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))))))))

View File

@ -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))

View File

@ -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 %])}})

View File

@ -2,7 +2,7 @@
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' 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"
}

View File

@ -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!')

View File

@ -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':

View File

@ -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