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:
parent
460685212a
commit
116b4d8812
|
@ -71,7 +71,6 @@
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity"/>
|
||||||
<service android:name="im.status.ethereum.pushnotifications.ForegroundService"></service>
|
<service android:name="im.status.ethereum.pushnotifications.ForegroundService"></service>
|
||||||
<service android:name="im.status.ethereum.module.LocalNotificationsService" />
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.provider"
|
android:authorities="${applicationId}.provider"
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -55,7 +55,7 @@ public class ForegroundService extends Service {
|
||||||
intent.setAction(Intent.ACTION_VIEW);
|
intent.setAction(Intent.ACTION_VIEW);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
|
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);
|
PendingIntent stopPendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||||
|
|
||||||
String content = context.getResources().getString(R.string.keep_status_running);
|
String content = context.getResources().getString(R.string.keep_status_running);
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -37,7 +37,7 @@ public class PushNotification extends ReactContextBaseJavaModule implements Acti
|
||||||
private PushNotificationHelper pushNotificationHelper;
|
private PushNotificationHelper pushNotificationHelper;
|
||||||
private PushNotificationJsDelivery delivery;
|
private PushNotificationJsDelivery delivery;
|
||||||
private ReactApplicationContext reactContext;
|
private ReactApplicationContext reactContext;
|
||||||
private NewMessageSignalHandler newMessageSignalHandler;
|
private boolean started;
|
||||||
|
|
||||||
public PushNotification(ReactApplicationContext reactContext) {
|
public PushNotification(ReactApplicationContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
@ -104,33 +104,30 @@ public class PushNotification extends ReactContextBaseJavaModule implements Acti
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void presentLocalNotification(ReadableMap details) {
|
public void presentLocalNotification(ReadableMap details) {
|
||||||
|
if (!this.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Bundle bundle = Arguments.toBundle(details);
|
Bundle bundle = Arguments.toBundle(details);
|
||||||
// If notification ID is not provided by the user, generate one at random
|
// If notification ID is not provided by the user, generate one at random
|
||||||
if (bundle.getString("id") == null) {
|
if (bundle.getString("id") == null) {
|
||||||
bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt()));
|
bundle.putString("id", String.valueOf(mRandomNumberGenerator.nextInt()));
|
||||||
}
|
}
|
||||||
|
|
||||||
String type = bundle.getString("type");
|
pushNotificationHelper.sendToNotificationCentre(bundle);
|
||||||
if (type != null && type.equals("message")) {
|
|
||||||
if (this.newMessageSignalHandler != null) {
|
|
||||||
newMessageSignalHandler.handleNewMessage(bundle);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pushNotificationHelper.sendToNotificationCentre(bundle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void enableNotifications() {
|
public void enableNotifications() {
|
||||||
this.newMessageSignalHandler = new NewMessageSignalHandler(reactContext);
|
this.started = true;
|
||||||
|
this.pushNotificationHelper.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void disableNotifications() {
|
public void disableNotifications() {
|
||||||
if (newMessageSignalHandler != null) {
|
if (this.started) {
|
||||||
newMessageSignalHandler.stop();
|
this.started = false;
|
||||||
newMessageSignalHandler = null;
|
this.pushNotificationHelper.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ import android.app.NotificationManager;
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
@ -24,9 +26,12 @@ import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.service.notification.StatusBarNotification;
|
import android.service.notification.StatusBarNotification;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
import android.util.Base64;
|
||||||
|
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.core.app.NotificationCompat;
|
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.Arguments;
|
||||||
import com.facebook.react.bridge.ReadableArray;
|
import com.facebook.react.bridge.ReadableArray;
|
||||||
|
@ -43,8 +48,10 @@ import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import im.status.ethereum.module.R;
|
||||||
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
import static im.status.ethereum.pushnotifications.PushNotification.LOG_TAG;
|
||||||
|
|
||||||
public class PushNotificationHelper {
|
public class PushNotificationHelper {
|
||||||
|
@ -53,11 +60,91 @@ public class PushNotificationHelper {
|
||||||
|
|
||||||
private static final long DEFAULT_VIBRATION = 300L;
|
private static final long DEFAULT_VIBRATION = 300L;
|
||||||
private static final String CHANNEL_ID = "status-im-notifications";
|
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) {
|
public PushNotificationHelper(Application context) {
|
||||||
this.context = 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() {
|
private NotificationManager notificationManager() {
|
||||||
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +182,12 @@ public class PushNotificationHelper {
|
||||||
aggregator.setBigPictureUrl(context, bundle.getString("bigPictureUrl"));
|
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) {
|
public void sendToNotificationCentreWithPicture(final Bundle bundle, Bitmap largeIconBitmap, Bitmap bigPictureBitmap) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Class intentClass = getMainActivityClass();
|
Class intentClass = getMainActivityClass();
|
||||||
if (intentClass == null) {
|
if (intentClass == null) {
|
||||||
|
@ -103,6 +195,11 @@ public class PushNotificationHelper {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bundle.getBoolean("isConversation")) {
|
||||||
|
this.handleConversation(bundle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (bundle.getString("message") == null) {
|
if (bundle.getString("message") == null) {
|
||||||
// this happens when a 'data' notification is received - we do not synthesize a local notification in this case
|
// 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);
|
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,
|
notification.setContentIntent(createOnTapIntent(context, notificationID, bundle.getString("deepLink")))
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
.setDeleteIntent(createOnDismissedIntent(context, notificationID, bundle.getString("deepLink")));
|
||||||
|
|
||||||
NotificationManager notificationManager = notificationManager();
|
NotificationManager notificationManager = notificationManager();
|
||||||
|
|
||||||
|
@ -367,7 +464,6 @@ public class PushNotificationHelper {
|
||||||
|
|
||||||
notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false));
|
notification.setUsesChronometer(bundle.getBoolean("usesChronometer", false));
|
||||||
notification.setChannelId(channel_id);
|
notification.setChannelId(channel_id);
|
||||||
notification.setContentIntent(pendingIntent);
|
|
||||||
|
|
||||||
JSONArray actionsArray = null;
|
JSONArray actionsArray = null;
|
||||||
try {
|
try {
|
||||||
|
@ -546,4 +642,155 @@ public class PushNotificationHelper {
|
||||||
return false;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,8 +30,6 @@
|
||||||
(status/set-soft-input-mode status/adjust-resize))
|
(status/set-soft-input-mode status/adjust-resize))
|
||||||
(.registerComponent ^js (.-AppRegistry rn) "StatusIm" #(reagent/reactify-component views/root))
|
(.registerComponent ^js (.-AppRegistry rn) "StatusIm" #(reagent/reactify-component views/root))
|
||||||
(notifications/listen-notifications)
|
(notifications/listen-notifications)
|
||||||
(when platform/android?
|
|
||||||
(.registerHeadlessTask ^js (.-AppRegistry rn) "LocalNotifications" notifications/handle))
|
|
||||||
(snoopy/subscribe!)
|
(snoopy/subscribe!)
|
||||||
(when (and js/goog.DEBUG platform/ios? DevSettings)
|
(when (and js/goog.DEBUG platform/ios? DevSettings)
|
||||||
;;on Android this method doesn't work
|
;;on Android this method doesn't work
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
(ns status-im.notifications.local
|
(ns status-im.notifications.local
|
||||||
(:require [taoensso.timbre :as log]
|
(:require [status-im.utils.fx :as fx]
|
||||||
[status-im.utils.fx :as fx]
|
|
||||||
[status-im.ethereum.decode :as decode]
|
[status-im.ethereum.decode :as decode]
|
||||||
["@react-native-community/push-notification-ios" :default pn-ios]
|
["@react-native-community/push-notification-ios" :default pn-ios]
|
||||||
[status-im.notifications.android :as pn-android]
|
[status-im.notifications.android :as pn-android]
|
||||||
|
@ -14,11 +13,8 @@
|
||||||
[re-frame.core :as re-frame]
|
[re-frame.core :as re-frame]
|
||||||
[status-im.ui.components.react :as react]
|
[status-im.ui.components.react :as react]
|
||||||
[cljs-bean.core :as bean]
|
[cljs-bean.core :as bean]
|
||||||
[status-im.ui.screens.chat.components.reply :as reply]
|
|
||||||
[clojure.string :as clojure.string]
|
[clojure.string :as clojure.string]
|
||||||
[status-im.chat.models :as chat.models]
|
[status-im.chat.models :as chat.models]))
|
||||||
[status-im.constants :as constants]
|
|
||||||
[status-im.utils.identicon :as identicon]))
|
|
||||||
|
|
||||||
(def default-erc20-token
|
(def default-erc20-token
|
||||||
{:symbol :ERC20
|
{:symbol :ERC20
|
||||||
|
@ -39,21 +35,8 @@
|
||||||
{:notificationType "local-notification"}))})))
|
{:notificationType "local-notification"}))})))
|
||||||
|
|
||||||
(defn local-push-android
|
(defn local-push-android
|
||||||
[{:keys [title message icon user-info channel-id type]
|
[notification]
|
||||||
:as notification
|
(pn-android/present-local-notification 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)))))
|
|
||||||
|
|
||||||
(defn handle-notification-press [{{deep-link :deepLink} :userInfo
|
(defn handle-notification-press [{{deep-link :deepLink} :userInfo
|
||||||
interaction :userInteraction}]
|
interaction :userInteraction}]
|
||||||
|
@ -106,65 +89,16 @@
|
||||||
nil)]
|
nil)]
|
||||||
{:title title
|
{:title title
|
||||||
:icon (get-in token [:icon :source])
|
:icon (get-in token [:icon :source])
|
||||||
|
:deepLink (:deepLink notification)
|
||||||
:user-info notification
|
:user-info notification
|
||||||
:message description}))
|
: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?
|
(defn show-message-pn?
|
||||||
[{{:keys [app-state]} :db :as cofx}
|
[{{:keys [app-state]} :db :as cofx}
|
||||||
{{:keys [chat-id]} :chat}]
|
notification]
|
||||||
(or (= app-state "background")
|
(let [chat-id (get-in notification [:body :chat :id])]
|
||||||
(not (chat.models/foreground-chat? cofx chat-id))))
|
(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))}))))
|
|
||||||
|
|
||||||
(defn create-notification
|
(defn create-notification
|
||||||
([notification]
|
([notification]
|
||||||
|
@ -172,7 +106,7 @@
|
||||||
([cofx {:keys [bodyType] :as notification}]
|
([cofx {:keys [bodyType] :as notification}]
|
||||||
(assoc
|
(assoc
|
||||||
(case bodyType
|
(case bodyType
|
||||||
"message" (create-message-notification cofx notification)
|
"message" (when (show-message-pn? cofx notification) notification)
|
||||||
"transaction" (create-transfer-notification notification)
|
"transaction" (create-transfer-notification notification)
|
||||||
nil)
|
nil)
|
||||||
:body-type bodyType)))
|
:body-type bodyType)))
|
||||||
|
@ -194,16 +128,3 @@
|
||||||
(if platform/ios?
|
(if platform/ios?
|
||||||
{::local-push-ios evt}
|
{::local-push-ios evt}
|
||||||
(local-notification-android cofx 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))))))))
|
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
"b/" browser-extractor
|
"b/" browser-extractor
|
||||||
"browser/" browser-extractor
|
"browser/" browser-extractor
|
||||||
["p/" :chat-id] :private-chat
|
["p/" :chat-id] :private-chat
|
||||||
|
["cr/" :community-id] :community-requests
|
||||||
"g/" group-chat-extractor
|
"g/" group-chat-extractor
|
||||||
["wallet/" :account] :wallet-account
|
["wallet/" :account] :wallet-account
|
||||||
["u/" :user-id] :user
|
["u/" :user-id] :user
|
||||||
|
@ -201,6 +202,9 @@
|
||||||
(spec/valid? :global/public-key uri)
|
(spec/valid? :global/public-key uri)
|
||||||
(match-contact-async chain {:user-id uri} cb)
|
(match-contact-async chain {:user-id uri} cb)
|
||||||
|
|
||||||
|
(= handler :community-requests)
|
||||||
|
(cb {:type handler :community-id (:community-id route-params)})
|
||||||
|
|
||||||
(= handler :referrals)
|
(= handler :referrals)
|
||||||
(cb (match-referral route-params))
|
(cb (match-referral route-params))
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,12 @@
|
||||||
(def domains {:external "https://join.status.im"
|
(def domains {:external "https://join.status.im"
|
||||||
:internal "status-im:/"})
|
:internal "status-im:/"})
|
||||||
|
|
||||||
(def links {:public-chat "%s/%s"
|
(def links {:public-chat "%s/%s"
|
||||||
:private-chat "%s/p/%s"
|
:private-chat "%s/p/%s"
|
||||||
:group-chat "%s/g/%s"
|
:community-requests "%s/cr/%s"
|
||||||
:user "%s/u/%s"
|
:group-chat "%s/g/%s"
|
||||||
:browse "%s/b/%s"})
|
:user "%s/u/%s"
|
||||||
|
:browse "%s/b/%s"})
|
||||||
|
|
||||||
(defn generate-link [link-type domain-type param]
|
(defn generate-link [link-type domain-type param]
|
||||||
(gstring/format (get links link-type)
|
(gstring/format (get links link-type)
|
||||||
|
@ -59,6 +60,10 @@
|
||||||
{:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code)
|
{:utils/show-popup {:title (i18n/label :t/unable-to-read-this-code)
|
||||||
:content (i18n/label :t/can-not-add-yourself)}})))
|
: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]}]
|
(fx/defn handle-public-chat [cofx {:keys [topic]}]
|
||||||
(log/info "universal-links: handling public chat" topic)
|
(log/info "universal-links: handling public chat" topic)
|
||||||
(when (seq topic)
|
(when (seq topic)
|
||||||
|
@ -113,20 +118,22 @@
|
||||||
{:events [::match-value]}
|
{:events [::match-value]}
|
||||||
[cofx url {:keys [type] :as data}]
|
[cofx url {:keys [type] :as data}]
|
||||||
(case type
|
(case type
|
||||||
:group-chat (handle-group-chat cofx data)
|
:group-chat (handle-group-chat cofx data)
|
||||||
:public-chat (handle-public-chat cofx data)
|
:public-chat (handle-public-chat cofx data)
|
||||||
:private-chat (handle-private-chat cofx data)
|
:private-chat (handle-private-chat cofx data)
|
||||||
:contact (handle-view-profile cofx data)
|
:community-requests (handle-community-requests cofx data)
|
||||||
:browser (handle-browse cofx data)
|
:contact (handle-view-profile cofx data)
|
||||||
:eip681 (handle-eip681 cofx data)
|
:browser (handle-browse cofx data)
|
||||||
:referrals (handle-referrer-url cofx data)
|
:eip681 (handle-eip681 cofx data)
|
||||||
:wallet-account (handle-wallet-account cofx data)
|
:referrals (handle-referrer-url cofx data)
|
||||||
|
:wallet-account (handle-wallet-account cofx data)
|
||||||
(handle-not-found url)))
|
(handle-not-found url)))
|
||||||
|
|
||||||
(fx/defn route-url
|
(fx/defn route-url
|
||||||
"Match a url against a list of routes and handle accordingly"
|
"Match a url against a list of routes and handle accordingly"
|
||||||
[{:keys [db]} url]
|
[{:keys [db]} url]
|
||||||
{::router/handle-uri {:chain (ethereum/chain-keyword db)
|
{::router/handle-uri {:chain (ethereum/chain-keyword db)
|
||||||
|
:chats (:chats db)
|
||||||
:uri url
|
:uri url
|
||||||
:cb #(re-frame/dispatch [::match-value url %])}})
|
:cb #(re-frame/dispatch [::match-value url %])}})
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
|
"_comment": "DO NOT EDIT THIS FILE BY HAND. USE 'scripts/update-status-go.sh <tag>' instead",
|
||||||
"owner": "status-im",
|
"owner": "status-im",
|
||||||
"repo": "status-go",
|
"repo": "status-go",
|
||||||
"version": "v0.74.1",
|
"version": "v0.74.2",
|
||||||
"commit-sha1": "5a76e93063e3b9ef2ded83a5f545f7149401e8f0",
|
"commit-sha1": "1724ecffa178e9bcb04ae6d9d95d276c02878b6f",
|
||||||
"src-sha256": "0581048fvqg0fxv03pm9a0drfiq32slxliwamnch3052avpdkycj"
|
"src-sha256": "1kn6996brpva1bdj8bv1mscwx17hynqk8xb667ai6qpdriscr22d"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.just_fyi('check that PN is received and after tap you are redirected to public chat')
|
||||||
home_1.open_notification_bar()
|
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()
|
chat_1.element_starts_with_text(user_1['ens'] +'.stateofus.eth','button').click()
|
||||||
if not profile_1.contacts_button.is_element_displayed():
|
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!')
|
self.errors.append('Was not redirected to own profile after tapping on mention of myself from another user!')
|
||||||
|
|
|
@ -57,6 +57,7 @@ class TestGroupChatMultipleDevice(MultipleDeviceTestCase):
|
||||||
for chat in (device_1_chat, device_2_chat):
|
for chat in (device_1_chat, device_2_chat):
|
||||||
if not chat.chat_element_by_text(join_system_message).is_element_displayed():
|
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')
|
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
|
message_1 = "Message from device: %s" % device_1_chat.driver.number
|
||||||
device_1_chat.send_message(message_1)
|
device_1_chat.send_message(message_1)
|
||||||
if device_1_chat.chat_element_by_text(message_1).status != 'delivered':
|
if device_1_chat.chat_element_by_text(message_1).status != 'delivered':
|
||||||
|
|
|
@ -360,7 +360,7 @@ class TestMessagesOneToOneChatMultiple(MultipleDeviceTestCase):
|
||||||
chat_1.send_message_button.click()
|
chat_1.send_message_button.click()
|
||||||
|
|
||||||
device_2.open_notification_bar()
|
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
|
listen_time = 5
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue