diff --git a/.env.release b/.env.release
index 8015cb8769..eeba5520bc 100644
--- a/.env.release
+++ b/.env.release
@@ -17,3 +17,4 @@ SNOOPY=0
RPC_NETWORKS_ONLY=1
PARTITIONED_TOPIC=0
MOBILE_UI_FOR_DESKTOP=1
+LOCAL_NOTIFICATIONs=0
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index cc5fc3c932..ff89606f45 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -4,6 +4,7 @@
package="im.status.ethereum">
+
@@ -68,6 +69,7 @@
+
= Build.VERSION_CODES.O) {
+ NotificationManager notificationManager =
+ context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(new NotificationChannel(CHANNEL_ID,
+ "Status Service",
+ NotificationManager.IMPORTANCE_HIGH));
+ }
+ String content = "Keep Status running to receive notifications";
+ Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_stat_notify_status)
+ .setContentTitle("Background notification service opened")
+ .setContentText(content)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .build();
+ // the id of the foreground notification MUST NOT be 0
+ startForeground(1, notification);
+ return START_STICKY;
+ }
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NewMessageSignalHandler.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NewMessageSignalHandler.java
new file mode 100644
index 0000000000..155de35bc0
--- /dev/null
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/NewMessageSignalHandler.java
@@ -0,0 +1,313 @@
+package im.status.ethereum.module;
+
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.Intent;
+
+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 androidx.core.app.NotificationManagerCompat;
+import androidx.core.app.NotificationCompat;
+
+import android.os.Build;
+import android.net.Uri;
+import android.media.AudioAttributes;
+
+import android.util.Log;
+
+class NewMessageSignalHandler {
+ private static final String GROUP_STATUS_MESSAGES = "im.status.notifications.message";
+ private static final String CHANNEL_NAME = "Status";
+ private static final String CHANNEL_DESCRIPTION = "Get notifications on new messages and mentions";
+ private static final String CHANNEL_ID = "status-chat-notifications";
+ private static final String TAG = "StatusModule";
+ private NotificationManager notificationManager;
+ private HashMap persons;
+ private HashMap chats;
+ private Context context;
+ private Intent serviceIntent;
+ private Boolean shouldRefreshNotifications;
+
+ public NewMessageSignalHandler(Context context) {
+ // NOTE: when instanciated the NewMessageSignalHandler class starts a foreground service
+ // to keep the app running in the background in order to receive notifications
+ // call the stop() method in order to stop the service
+ this.context = context;
+ this.persons = new HashMap();
+ this.chats = new HashMap();
+ this.notificationManager = context.getSystemService(NotificationManager.class);
+ this.createNotificationChannel();
+ this.shouldRefreshNotifications = false;
+ Log.e(TAG, "Starting Foreground Service");
+ Intent serviceIntent = new Intent(context, ForegroundService.class);
+ context.startService(serviceIntent);
+ }
+
+ public void stop() {
+ Log.e(TAG, "Stopping Foreground Service");
+ Intent serviceIntent = new Intent(context, ForegroundService.class);
+ context.stopService(serviceIntent);
+ }
+
+ 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(CHANNEL_DESCRIPTION);
+ AudioAttributes audioAttributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .build();
+ channel.setSound(soundUri, audioAttributes);
+ NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
+ notificationManager.createNotificationChannel(channel);
+ }
+ }
+
+ public void notify(int notificationId, StatusChat chat) {
+ NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle("Me");
+ ArrayList messages = chat.getMessages();
+ for (int i = 0; i < messages.size(); i++) {
+ StatusMessage message = messages.get(i);
+ messagingStyle.addMessage(message.getText(),
+ message.getTimestamp(),
+ message.getAuthor());
+ }
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.ic_stat_notify_status)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setStyle(messagingStyle)
+ .setGroup(GROUP_STATUS_MESSAGES);
+ 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();
+ String summary = "";
+ int notificationId = 2; // we start at 2 because the service is using 1 and can't use 0
+ int messageCounter = 0;
+ Iterator chatIterator = chats.values().iterator();
+ while(chatIterator.hasNext()) {
+ StatusChat chat = (StatusChat)chatIterator.next();
+ notify(notificationId, chat);
+ notificationId++;
+ messageCounter += chat.getMessages().size();
+ summaryStyle.addLine(chat.getSummary());
+ summary += chat.getSummary() + "\n";
+ }
+ // NOTE: this is necessary for old versions of Android, newer versions are
+ // building this group themselves and I was not able to make any change to
+ // what this group displays
+ NotificationCompat.Builder groupBuilder =
+ new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setContentText(summary)
+ .setSmallIcon(R.drawable.ic_stat_notify_status)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_MESSAGE)
+ .setContentTitle("You got " + messageCounter + " messages in " + chats.size() + " chats")
+ .setStyle(summaryStyle
+ .setBigContentTitle("You got " + messageCounter + " messages in " + chats.size() + " chats"))
+ .setGroup(GROUP_STATUS_MESSAGES)
+ .setGroupSummary(true);
+ notificationManager.notify(notificationId, groupBuilder.build());
+ }
+
+ void handleNewMessageSignal(JSONObject newMessageSignal) {
+ try {
+ JSONArray chatsNewMessagesData = newMessageSignal.getJSONObject("event").getJSONArray("messages");
+ for (int i = 0; i < chatsNewMessagesData.length(); i++) {
+ try {
+ upsertChat(chatsNewMessagesData.getJSONObject(i));
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON conversion failed: " + e.getMessage());
+ }
+ }
+ if(shouldRefreshNotifications) {
+ refreshNotifications();
+ shouldRefreshNotifications = false;
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON conversion failed: " + e.getMessage());
+ }
+ }
+
+ 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(JSONObject chatNewMessagesData) {
+ try {
+ JSONObject chatData = chatNewMessagesData.getJSONObject("chat");
+ // NOTE: this is an exemple of chatData
+ // {"chatId":"contact-discovery-3622","filterId":"c0239d63f830e8b25f4bf7183c8d207f355a925b89514a17068cae4898e7f193",
+ // "symKeyId":"","oneToOne":true,"identity":"046599511623d7385b926ce709ac57d518dac10d451a81f75cd32c7fb4b1c...",
+ // "topic":"0xc446561b","discovery":false,"negotiated":false,"listen":true}
+ boolean oneToOne = chatData.getBoolean("oneToOne");
+ // NOTE: for now we only notify one to one chats
+ // TODO: also notifiy on mentions, keywords and group chats
+ // TODO: one would have to pass the ens name and keywords to notify on when instanciating the class as well
+ // as have a method to add new ones after the handler is instanciated
+ if (oneToOne) {
+ JSONArray messagesData = chatNewMessagesData.getJSONArray("messages");
+
+ // there is no proper id for oneToOne chat in chatData so we peek into first message sig
+ // TODO: won't work for sync becaus it could be our own message
+ String id = messagesData.getJSONObject(0).getJSONObject("message").getString("sig");
+ StatusChat chat = chats.get(id);
+
+
+ // if the chat was not already there, we create one
+ if (chat == null) {
+ chat = new StatusChat(id, oneToOne);
+ }
+
+ ArrayList messages = chat.getMessages();
+ // parse the new messages
+ for (int j = 0; j < messagesData.length(); j++) {
+ StatusMessage message = createMessage(messagesData.getJSONObject(j));
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+
+ if (!messages.isEmpty()) {
+ chat.setMessages(messages);
+ chats.put(id, chat);
+ shouldRefreshNotifications = true;
+ }
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON conversion failed: " + e.getMessage());
+ }
+ }
+
+ private StatusMessage createMessage(JSONObject messageData) {
+ try {
+ JSONObject metadata = messageData.getJSONObject("metadata");
+ JSONObject authorMetadata = metadata.getJSONObject("author");
+ JSONArray payload = new JSONArray(messageData.getString("payload"));
+ // NOTE: this is an exemple of payload we are currently working with
+ // it is in the transit format, which is basically JSON
+ // refer to `transport.message.transit.cljs` on react side for details
+ // ["~#c4",["7","text/plain","~:public-group-user-message",157201130275201,1572011302752,["^ ","~:chat-id","test","~:text","7"]]]
+ if (payload.getString(0).equals("~#c4")) {
+ Person author = getPerson(authorMetadata.getString("publicKey"), authorMetadata.getString("identicon"), authorMetadata.getString("alias"));
+ JSONArray payloadContent = payload.getJSONArray(1);
+ String text = payloadContent.getString(0);
+ Double timestamp = payloadContent.getDouble(4);
+ return new StatusMessage(author, timestamp.longValue(), text);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON conversion failed: " + e.getMessage());
+ }
+ return null;
+ }
+}
+
+class StatusChat {
+ private ArrayList messages;
+ private String id;
+ private String name;
+ private Boolean oneToOne;
+
+ StatusChat(String id, Boolean oneToOne) {
+ this.id = id;
+ this.oneToOne = oneToOne;
+ this.messages = new ArrayList();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ 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
+ return getLastMessage().getAuthor().getName().toString();
+ }
+
+ private StatusMessage getLastMessage() {
+ return messages.get(messages.size()-1);
+ }
+
+ public long getTimestamp() {
+ return getLastMessage().getTimestamp();
+ }
+
+ public ArrayList getMessages() {
+ return messages;
+ }
+
+ public void setMessages(ArrayList messages) {
+ this.messages = messages;
+ }
+
+ public String getSummary() {
+ return "" + getLastMessage().getAuthor().getName() + ": " + getLastMessage().getText();
+ }
+}
+
+
+class StatusMessage {
+ public Person getAuthor() {
+ return author;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ private Person author;
+ private long timestamp;
+ private String text;
+
+ StatusMessage(Person author, long timestamp, String text) {
+ this.author = author;
+ this.timestamp = timestamp;
+ this.text = text;
+ }
+}
diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
index 25352fabe4..9270bce90a 100644
--- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
+++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java
@@ -10,7 +10,9 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.preference.PreferenceManager;
+
import androidx.core.content.FileProvider;
+
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
@@ -24,6 +26,8 @@ import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableMap;
+import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -32,6 +36,8 @@ import statusgo.Statusgo;
import org.json.JSONException;
import org.json.JSONObject;
+import org.json.JSONArray;
+
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@@ -51,16 +57,18 @@ import java.util.zip.ZipOutputStream;
import javax.annotation.Nullable;
+import android.app.Service;
+
class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventListener, SignalHandler {
private static final String TAG = "StatusModule";
private static final String logsZipFileName = "Status-debug-logs.zip";
private static final String gethLogFileName = "geth.log";
private static final String statusLogFileName = "Status.log";
-
private static StatusModule module;
private ReactApplicationContext reactContext;
private boolean rootedDevice;
+ private NewMessageSignalHandler newMessageSignalHandler;
StatusModule(ReactApplicationContext reactContext, boolean rootedDevice) {
super(reactContext);
@@ -82,46 +90,72 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
@Override
public void onHostPause() {
-
}
@Override
public void onHostDestroy() {
+ Intent intent = new Intent(getReactApplicationContext(), ForegroundService.class);
+ getReactApplicationContext().stopService(intent);
+ }
+ @ReactMethod
+ public void enableNotifications() {
+ this.newMessageSignalHandler = new NewMessageSignalHandler(reactContext);
+ }
+
+ @ReactMethod
+ public void disableNotifications() {
+ if (newMessageSignalHandler != null) {
+ newMessageSignalHandler.stop();
+ newMessageSignalHandler = null;
+ }
}
private boolean checkAvailability() {
- // We wait at least 10s for getCurrentActivity to return a value,
- // otherwise we give up
- for (int attempts = 0; attempts < 100; attempts++) {
- if (getCurrentActivity() != null) {
- return true;
+ // We wait at least 10s for getCurrentActivity to return a value,
+ // otherwise we give up
+ for (int attempts = 0; attempts < 100; attempts++) {
+ if (getCurrentActivity() != null) {
+ return true;
+ }
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException ex) {
+ if (getCurrentActivity() != null) {
+ return true;
+ }
+ Log.d(TAG, "Activity doesn't exist");
+ return false;
+ }
}
- try {
- Thread.sleep(100);
- } catch (InterruptedException ex) {
- if (getCurrentActivity() != null) {
- return true;
- }
- Log.d(TAG, "Activity doesn't exist");
- return false;
- }
- }
- Log.d(TAG, "Activity doesn't exist");
- return false;
+ Log.d(TAG, "Activity doesn't exist");
+ return false;
}
public String getNoBackupDirectory() {
- return this.getReactApplicationContext().getNoBackupFilesDir().getAbsolutePath();
+ return this.getReactApplicationContext().getNoBackupFilesDir().getAbsolutePath();
}
- public void handleSignal(String jsonEvent) {
- Log.d(TAG, "Signal event: " + jsonEvent);
- WritableMap params = Arguments.createMap();
- params.putString("jsonEvent", jsonEvent);
- this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params);
+ public void handleSignal(final String jsonEventString) {
+ try {
+ final JSONObject jsonEvent = new JSONObject(jsonEventString);
+ String eventType = jsonEvent.getString("type");
+ Log.d(TAG, "Signal event: " + jsonEventString);
+ // NOTE: the newMessageSignalHandler is only instanciated if the user
+ // enabled notifications in the app
+ if (newMessageSignalHandler != null) {
+ if (eventType.equals("messages.new")) {
+ newMessageSignalHandler.handleNewMessageSignal(jsonEvent);
+ }
+ }
+ WritableMap params = Arguments.createMap();
+ params.putString("jsonEvent", jsonEventString);
+ this.getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("gethEvent", params);
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON conversion failed: " + e.getMessage());
+ }
}
private File getLogsFile() {
@@ -278,29 +312,27 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
@ReactMethod
- public void saveAccountAndLogin(final String accountData, final String password , final String config, final String subAccountsData) {
+ public void saveAccountAndLogin(final String accountData, final String password, final String config, final String subAccountsData) {
Log.d(TAG, "saveAccountAndLogin");
String finalConfig = prepareDirAndUpdateConfig(config);
String result = Statusgo.saveAccountAndLogin(accountData, password, finalConfig, subAccountsData);
if (result.startsWith("{\"error\":\"\"")) {
Log.d(TAG, "StartNode result: " + result);
Log.d(TAG, "Geth node started");
- }
- else {
+ } else {
Log.e(TAG, "StartNode failed: " + result);
}
}
@ReactMethod
- public void saveAccountAndLoginWithKeycard(final String accountData, final String password , final String config, final String chatKey) {
+ public void saveAccountAndLoginWithKeycard(final String accountData, final String password, final String config, final String chatKey) {
Log.d(TAG, "saveAccountAndLoginWithKeycard");
String finalConfig = prepareDirAndUpdateConfig(config);
String result = Statusgo.saveAccountAndLoginWithKeycard(accountData, password, finalConfig, chatKey);
if (result.startsWith("{\"error\":\"\"")) {
Log.d(TAG, "StartNode result: " + result);
Log.d(TAG, "Geth node started");
- }
- else {
+ } else {
Log.e(TAG, "StartNode failed: " + result);
}
}
@@ -311,8 +343,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
String result = Statusgo.login(accountData, password);
if (result.startsWith("{\"error\":\"\"")) {
Log.d(TAG, "Login result: " + result);
- }
- else {
+ } else {
Log.e(TAG, "Login failed: " + result);
}
}
@@ -320,11 +351,11 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
@ReactMethod
public void logout() {
Log.d(TAG, "logout");
+ disableNotifications();
String result = Statusgo.logout();
if (result.startsWith("{\"error\":\"\"")) {
Log.d(TAG, "Logout result: " + result);
- }
- else {
+ } else {
Log.e(TAG, "Logout failed: " + result);
}
}
@@ -387,11 +418,11 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
Runnable r = new Runnable() {
- @Override
- public void run() {
- Statusgo.initKeystore(keydir);
- }
- };
+ @Override
+ public void run() {
+ Statusgo.initKeystore(keydir);
+ }
+ };
StatusThreadPoolExecutor.getInstance().execute(r);
}
@@ -409,12 +440,12 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
Runnable r = new Runnable() {
- @Override
- public void run() {
- String result =Statusgo.openAccounts(rootDir);
- callback.invoke(result);
- }
- };
+ @Override
+ public void run() {
+ String result = Statusgo.openAccounts(rootDir);
+ callback.invoke(result);
+ }
+ };
StatusThreadPoolExecutor.getInstance().execute(r);
}
@@ -450,8 +481,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
String result = Statusgo.loginWithKeycard(accountData, password, chatKey);
if (result.startsWith("{\"error\":\"\"")) {
Log.d(TAG, "LoginWithKeycard result: " + result);
- }
- else {
+ } else {
Log.e(TAG, "LoginWithKeycard failed: " + result);
}
}
@@ -459,13 +489,13 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
private Boolean zip(File[] _files, File zipFile, Stack errorList) {
final int BUFFER = 0x8000;
- try {
- BufferedInputStream origin = null;
- FileOutputStream dest = new FileOutputStream(zipFile);
- ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
- byte data[] = new byte[BUFFER];
+ try {
+ BufferedInputStream origin = null;
+ FileOutputStream dest = new FileOutputStream(zipFile);
+ ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(dest));
+ byte data[] = new byte[BUFFER];
- for (int i = 0; i < _files.length; i++) {
+ for (int i = 0; i < _files.length; i++) {
final File file = _files[i];
if (file == null || !file.exists()) {
continue;
@@ -488,16 +518,16 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
Log.e(TAG, e.getMessage());
errorList.push(e.getMessage());
}
- }
+ }
out.close();
return true;
- } catch (Exception e) {
+ } catch (Exception e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
return false;
- }
+ }
}
private void dumpAdbLogsTo(final FileOutputStream statusLogStream) throws IOException {
@@ -518,13 +548,13 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
final Activity activity = getCurrentActivity();
new AlertDialog.Builder(activity)
- .setTitle("Error")
- .setMessage(message)
- .setNegativeButton("Exit", new DialogInterface.OnClickListener() {
- public void onClick(final DialogInterface dialog, final int id) {
- dialog.dismiss();
- }
- }).show();
+ .setTitle("Error")
+ .setMessage(message)
+ .setNegativeButton("Exit", new DialogInterface.OnClickListener() {
+ public void onClick(final DialogInterface dialog, final int id) {
+ dialog.dismiss();
+ }
+ }).show();
}
@ReactMethod
@@ -543,8 +573,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(dbFile));
outputStreamWriter.write(dbJson);
outputStreamWriter.close();
- }
- catch (IOException e) {
+ } catch (IOException e) {
Log.e(TAG, "File write failed: " + e.toString());
showErrorMessage(e.getLocalizedMessage());
}
@@ -567,7 +596,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
dumpAdbLogsTo(new FileOutputStream(statusLogFile));
final Stack errorList = new Stack();
- final Boolean zipped = zip(new File[] {dbFile, gethLogFile, statusLogFile}, zipFile, errorList);
+ final Boolean zipped = zip(new File[]{dbFile, gethLogFile, statusLogFile}, zipFile, errorList);
if (zipped && zipFile.exists()) {
zipFile.setReadable(true, false);
callback.invoke(zipFile.getAbsolutePath());
@@ -579,8 +608,7 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
showErrorMessage(e.getLocalizedMessage());
e.printStackTrace();
return;
- }
- finally {
+ } finally {
dbFile.delete();
statusLogFile.delete();
zipFile.deleteOnExit();
@@ -596,13 +624,13 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
Runnable r = new Runnable() {
- @Override
- public void run() {
- String res = Statusgo.addPeer(enode);
+ @Override
+ public void run() {
+ String res = Statusgo.addPeer(enode);
- callback.invoke(res);
- }
- };
+ callback.invoke(res);
+ }
+ };
StatusThreadPoolExecutor.getInstance().execute(r);
}
@@ -1026,138 +1054,138 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL
}
}
- private Boolean is24Hour() {
- return android.text.format.DateFormat.is24HourFormat(this.reactContext.getApplicationContext());
- }
-
- @ReactMethod
- public void extractGroupMembershipSignatures(final String signaturePairs, final Callback callback) {
- Log.d(TAG, "extractGroupMembershipSignatures");
- if (!checkAvailability()) {
- callback.invoke(false);
- return;
+ private Boolean is24Hour() {
+ return android.text.format.DateFormat.is24HourFormat(this.reactContext.getApplicationContext());
}
- Runnable r = new Runnable() {
- @Override
- public void run() {
- String result = Statusgo.extractGroupMembershipSignatures(signaturePairs);
+ @ReactMethod
+ public void extractGroupMembershipSignatures(final String signaturePairs, final Callback callback) {
+ Log.d(TAG, "extractGroupMembershipSignatures");
+ if (!checkAvailability()) {
+ callback.invoke(false);
+ return;
+ }
- callback.invoke(result);
- }
- };
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ String result = Statusgo.extractGroupMembershipSignatures(signaturePairs);
- StatusThreadPoolExecutor.getInstance().execute(r);
- }
+ callback.invoke(result);
+ }
+ };
- @ReactMethod
- public void signGroupMembership(final String content, final Callback callback) {
- Log.d(TAG, "signGroupMembership");
- if (!checkAvailability()) {
- callback.invoke(false);
- return;
+ StatusThreadPoolExecutor.getInstance().execute(r);
}
- Runnable r = new Runnable() {
- @Override
- public void run() {
- String result = Statusgo.signGroupMembership(content);
+ @ReactMethod
+ public void signGroupMembership(final String content, final Callback callback) {
+ Log.d(TAG, "signGroupMembership");
+ if (!checkAvailability()) {
+ callback.invoke(false);
+ return;
+ }
- callback.invoke(result);
- }
- };
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ String result = Statusgo.signGroupMembership(content);
- StatusThreadPoolExecutor.getInstance().execute(r);
- }
+ callback.invoke(result);
+ }
+ };
-
- @ReactMethod
- public void updateMailservers(final String enodes, final Callback callback) {
- Log.d(TAG, "updateMailservers");
- if (!checkAvailability()) {
- callback.invoke(false);
- return;
+ StatusThreadPoolExecutor.getInstance().execute(r);
}
- Runnable r = new Runnable() {
- @Override
- public void run() {
- String res = Statusgo.updateMailservers(enodes);
- callback.invoke(res);
- }
- };
+ @ReactMethod
+ public void updateMailservers(final String enodes, final Callback callback) {
+ Log.d(TAG, "updateMailservers");
+ if (!checkAvailability()) {
+ callback.invoke(false);
+ return;
+ }
- StatusThreadPoolExecutor.getInstance().execute(r);
- }
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ String res = Statusgo.updateMailservers(enodes);
- @ReactMethod
- public void chaosModeUpdate(final boolean on, final Callback callback) {
- Log.d(TAG, "chaosModeUpdate");
- if (!checkAvailability()) {
- callback.invoke(false);
- return;
+ callback.invoke(res);
+ }
+ };
+
+ StatusThreadPoolExecutor.getInstance().execute(r);
}
- Runnable r = new Runnable() {
- @Override
- public void run() {
- String res = Statusgo.chaosModeUpdate(on);
+ @ReactMethod
+ public void chaosModeUpdate(final boolean on, final Callback callback) {
+ Log.d(TAG, "chaosModeUpdate");
+ if (!checkAvailability()) {
+ callback.invoke(false);
+ return;
+ }
- callback.invoke(res);
- }
- };
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ String res = Statusgo.chaosModeUpdate(on);
- StatusThreadPoolExecutor.getInstance().execute(r);
- }
+ callback.invoke(res);
+ }
+ };
- @ReactMethod(isBlockingSynchronousMethod = true)
- public String generateAlias(final String seed) {
- return Statusgo.generateAlias(seed);
- }
-
- @ReactMethod(isBlockingSynchronousMethod = true)
- public String identicon(final String seed) {
- return Statusgo.identicon(seed);
- }
-
-
- @ReactMethod
- public void getNodesFromContract(final String rpcEndpoint, final String contractAddress, final Callback callback) {
- Log.d(TAG, "getNodesFromContract");
- if (!checkAvailability()) {
- callback.invoke(false);
- return;
+ StatusThreadPoolExecutor.getInstance().execute(r);
}
- Runnable r = new Runnable() {
- @Override
- public void run() {
- String res = Statusgo.getNodesFromContract(rpcEndpoint, contractAddress);
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ public String generateAlias(final String seed) {
+ return Statusgo.generateAlias(seed);
+ }
- Log.d(TAG, res);
- callback.invoke(res);
- }
- };
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ public String identicon(final String seed) {
+ return Statusgo.identicon(seed);
+ }
- StatusThreadPoolExecutor.getInstance().execute(r);
- }
- @Override
- public @Nullable
- Map getConstants() {
- HashMap constants = new HashMap();
+ @ReactMethod
+ public void getNodesFromContract(final String rpcEndpoint, final String contractAddress, final Callback callback) {
+ Log.d(TAG, "getNodesFromContract");
+ if (!checkAvailability()) {
+ callback.invoke(false);
+ return;
+ }
- constants.put("is24Hour", this.is24Hour());
- constants.put("model", Build.MODEL);
- constants.put("brand", Build.BRAND);
- constants.put("buildId", Build.ID);
- constants.put("deviceId", Build.BOARD);
- return constants;
- }
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ String res = Statusgo.getNodesFromContract(rpcEndpoint, contractAddress);
- @ReactMethod
- public void isDeviceRooted(final Callback callback) {
- callback.invoke(rootedDevice);
- }
+ Log.d(TAG, res);
+ callback.invoke(res);
+ }
+ };
+
+ StatusThreadPoolExecutor.getInstance().execute(r);
+ }
+
+ @Override
+ public @Nullable
+ Map getConstants() {
+ HashMap constants = new HashMap();
+
+ constants.put("is24Hour", this.is24Hour());
+ constants.put("model", Build.MODEL);
+ constants.put("brand", Build.BRAND);
+ constants.put("buildId", Build.ID);
+ constants.put("deviceId", Build.BOARD);
+ return constants;
+ }
+
+ @ReactMethod
+ public void isDeviceRooted(final Callback callback) {
+ callback.invoke(rootedDevice);
+ }
}
diff --git a/modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png
new file mode 100644
index 0000000000..6466c90c39
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png differ
diff --git a/modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png
new file mode 100644
index 0000000000..ef91b14a64
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png differ
diff --git a/modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png
new file mode 100644
index 0000000000..014de5e163
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png differ
diff --git a/modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png
new file mode 100644
index 0000000000..19432801ef
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png differ
diff --git a/modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png b/modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png
new file mode 100644
index 0000000000..911d98601a
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png differ
diff --git a/modules/react-native-status/android/src/main/res/drawable/notification_icon.png b/modules/react-native-status/android/src/main/res/drawable/notification_icon.png
new file mode 100644
index 0000000000..a040aa06c8
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/drawable/notification_icon.png differ
diff --git a/modules/react-native-status/android/src/main/res/raw/notification_sound.mp3 b/modules/react-native-status/android/src/main/res/raw/notification_sound.mp3
new file mode 100644
index 0000000000..81b702fd43
Binary files /dev/null and b/modules/react-native-status/android/src/main/res/raw/notification_sound.mp3 differ
diff --git a/nix/mobile/android/maven-and-npm-deps/maven/maven-inputs.txt b/nix/mobile/android/maven-and-npm-deps/maven/maven-inputs.txt
index 4701d34a02..e96c8148d9 100644
--- a/nix/mobile/android/maven-and-npm-deps/maven/maven-inputs.txt
+++ b/nix/mobile/android/maven-and-npm-deps/maven/maven-inputs.txt
@@ -61,6 +61,7 @@ https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.3.
https://dl.google.com/dl/android/maven2/com/android/databinding/baseLibrary/3.4.1/baseLibrary-3.4.1
https://dl.google.com/dl/android/maven2/com/android/databinding/compilerCommon/3.0.1/compilerCommon-3.0.1
https://dl.google.com/dl/android/maven2/com/android/support/appcompat-v7/28.0.0/appcompat-v7-28.0.0
+https://dl.google.com/dl/android/maven2/com/android/support/support-compat/28.0.0/support-compat-28.0.0
https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.2.1/crash-26.2.1
https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.3.1/crash-26.3.1
https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.4.1/crash-26.4.1
diff --git a/nix/mobile/android/maven-and-npm-deps/maven/maven-sources.nix b/nix/mobile/android/maven-and-npm-deps/maven/maven-sources.nix
index 3245c88600..c37d528316 100644
--- a/nix/mobile/android/maven-and-npm-deps/maven/maven-sources.nix
+++ b/nix/mobile/android/maven-and-npm-deps/maven/maven-sources.nix
@@ -965,6 +965,21 @@ in {
sha256 = "0lhp66q8rxf8cxylr8g6qjqy6s26prgrnmq133cnwx2r0ciyba53";
};
};
+ "https://dl.google.com/dl/android/maven2/com/android/support/support-compat/28.0.0/support-compat-28.0.0" =
+ {
+ host = repositories.google;
+ path =
+ "com/android/support/support-compat/28.0.0/support-compat-28.0.0";
+ type = "aar";
+ pom = {
+ sha1 = "ededbbdbfc461c09f992371624bf7fa564748c36";
+ sha256 = "06ln7psm2gm6nskdj48cgd2mrzs1mlk6m0px3jb0zz4249na0ybb";
+ };
+ jar = {
+ sha1 = "d252b640ed832cf8addc35ef0a9f9186dc7738a5";
+ sha256 = "12hi2xc9qshbdr2jw96664i3va9wj0pjjhv9r2hrwgzavc0knzp1";
+ };
+ };
"https://dl.google.com/dl/android/maven2/com/android/tools/analytics-library/crash/26.2.1/crash-26.2.1" =
{
host = repositories.google;
diff --git a/src/status_im/multiaccounts/core.cljs b/src/status_im/multiaccounts/core.cljs
index 13c5730138..0487908c14 100644
--- a/src/status_im/multiaccounts/core.cljs
+++ b/src/status_im/multiaccounts/core.cljs
@@ -5,6 +5,7 @@
[status-im.i18n :as i18n]
[status-im.multiaccounts.update.core :as multiaccounts.update]
[status-im.native-module.core :as native-module]
+ [status-im.notifications.core :as notifications]
[status-im.utils.build :as build]
[status-im.utils.config :as config]
[status-im.utils.fx :as fx]
@@ -65,51 +66,72 @@
{:dev-mode? dev-mode?}
{}))
+(fx/defn switch-notifications
+ {:events [:multiaccounts.ui/notifications-switched]}
+ [cofx notifications-enabled?]
+ (fx/merge cofx
+ {(if notifications-enabled?
+ ::notifications/enable
+ ::notifications/disable) nil}
+ (multiaccounts.update/multiaccount-update
+ {:notifications-enabled? notifications-enabled?}
+ {})))
+
(fx/defn switch-chaos-mode
[{:keys [db] :as cofx} chaos-mode?]
(when (:multiaccount db)
(fx/merge cofx
{::chaos-mode-changed chaos-mode?}
- (multiaccounts.update/multiaccount-update {:chaos-mode? chaos-mode?}
- {}))))
+ (multiaccounts.update/multiaccount-update
+ {:chaos-mode? chaos-mode?}
+ {}))))
(fx/defn enable-notifications [cofx desktop-notifications?]
- (multiaccounts.update/multiaccount-update cofx
- {:desktop-notifications? desktop-notifications?}
- {}))
+ (multiaccounts.update/multiaccount-update
+ cofx
+ {:desktop-notifications? desktop-notifications?}
+ {}))
(fx/defn toggle-datasync
[{:keys [db] :as cofx} enabled?]
(let [settings (get-in db [:multiaccount :settings])
- warning {:utils/show-popup {:title (i18n/label :t/datasync-warning-title)
- :content (i18n/label :t/datasync-warning-content)}}]
+ warning {:utils/show-popup
+ {:title (i18n/label :t/datasync-warning-title)
+ :content (i18n/label :t/datasync-warning-content)}}]
(fx/merge cofx
(when enabled? warning)
- (multiaccounts.update/update-settings (assoc settings :datasync? enabled?)
- {}))))
+ (multiaccounts.update/update-settings
+ (assoc settings :datasync? enabled?)
+ {}))))
(fx/defn toggle-v1-messages
[{:keys [db] :as cofx} enabled?]
(let [settings (get-in db [:multiaccount :settings])
- warning {:utils/show-popup {:title (i18n/label :t/v1-messages-warning-title)
- :content (i18n/label :t/v1-messages-warning-content)}}]
+ warning {:utils/show-popup
+ {:title (i18n/label :t/v1-messages-warning-title)
+ :content (i18n/label :t/v1-messages-warning-content)}}]
(fx/merge cofx
(when enabled? warning)
- (multiaccounts.update/update-settings (assoc settings :v1-messages? enabled?)
- {}))))
+ (multiaccounts.update/update-settings
+ (assoc settings :v1-messages? enabled?)
+ {}))))
(fx/defn toggle-disable-discovery-topic
[{:keys [db] :as cofx} enabled?]
(let [settings (get-in db [:multiaccount :settings])
- warning {:utils/show-popup {:title (i18n/label :t/disable-discovery-topic-warning-title)
- :content (i18n/label :t/disable-discovery-topic-warning-content)}}]
+ warning {:utils/show-popup
+ {:title
+ (i18n/label :t/disable-discovery-topic-warning-title)
+ :content
+ (i18n/label :t/disable-discovery-topic-warning-content)}}]
(fx/merge cofx
(when enabled? warning)
- (multiaccounts.update/update-settings (assoc settings :disable-discovery-topic? enabled?)
- {}))))
+ (multiaccounts.update/update-settings
+ (assoc settings :disable-discovery-topic? enabled?)
+ {}))))
(fx/defn switch-web3-opt-in-mode
[{:keys [db] :as cofx} opt-in]
diff --git a/src/status_im/multiaccounts/login/core.cljs b/src/status_im/multiaccounts/login/core.cljs
index ac327a3eaf..67b75bbf43 100644
--- a/src/status_im/multiaccounts/login/core.cljs
+++ b/src/status_im/multiaccounts/login/core.cljs
@@ -12,6 +12,7 @@
[status-im.i18n :as i18n]
[status-im.native-module.core :as status]
[status-im.node.core :as node]
+ [status-im.notifications.core :as notifications]
[status-im.protocol.core :as protocol]
[status-im.stickers.core :as stickers]
[status-im.ui.screens.mobile-network-settings.events :as mobile-network]
@@ -122,11 +123,12 @@
(when (not= network-id fetched-network-id)
;;TODO: this shouldn't happen but in case it does
;;we probably want a better error message
- (utils/show-popup (i18n/label :t/ethereum-node-started-incorrectly-title)
- (i18n/label :t/ethereum-node-started-incorrectly-description
- {:network-id network-id
- :fetched-network-id fetched-network-id})
- #(re-frame/dispatch [::close-app-confirmed]))))}]})
+ (utils/show-popup
+ (i18n/label :t/ethereum-node-started-incorrectly-title)
+ (i18n/label :t/ethereum-node-started-incorrectly-description
+ {:network-id network-id
+ :fetched-network-id fetched-network-id})
+ #(re-frame/dispatch [::close-app-confirmed]))))}]})
(defn deserialize-config
[{:keys [multiaccount current-network networks]}]
@@ -144,14 +146,18 @@
(fx/defn get-config-callback
{:events [::get-config-callback]}
[{:keys [db] :as cofx} config]
- (let [[{:keys [address] :as multiaccount} current-network networks] (deserialize-config config)
+ (let [[{:keys [address notifications-enabled?] :as multiaccount}
+ current-network networks] (deserialize-config config)
network-id (str (get-in networks [current-network :config :NetworkId]))]
(fx/merge cofx
- {:db (assoc db
- :networks/current-network current-network
- :networks/networks networks
- :multiaccount (convert-multiaccount-addresses
- multiaccount))}
+ (cond-> {:db (assoc db
+ :networks/current-network current-network
+ :networks/networks networks
+ :multiaccount (convert-multiaccount-addresses
+ multiaccount))}
+ (and platform/android?
+ notifications-enabled?)
+ (assoc ::notifications/enable nil))
;; NOTE: initializing mailserver depends on user mailserver
;; preference which is why we wait for config callback
(protocol/initialize-protocol {:default-mailserver true})
@@ -169,9 +175,15 @@
[{:keys [db] :as cofx} address password save-password?]
(let [auth-method (:auth-method db)
new-auth-method (if save-password?
- (when-not (or (= "biometric" auth-method) (= "password" auth-method))
- (if (= auth-method "biometric-prepare") "biometric" "password"))
- (when (and auth-method (not= auth-method "none")) "none"))]
+ (when-not (or (= "biometric" auth-method)
+ (= "password" auth-method))
+ (if (= auth-method "biometric-prepare")
+ "biometric"
+ "password"))
+ (when (and auth-method
+ (not= auth-method
+ "none"))
+ "none"))]
(fx/merge cofx
{:db (assoc db :chats/loading? true)
::json-rpc/call
diff --git a/src/status_im/native_module/core.cljs b/src/status_im/native_module/core.cljs
index 954e64b95e..135e6b7bfd 100644
--- a/src/status_im/native_module/core.cljs
+++ b/src/status_im/native_module/core.cljs
@@ -30,6 +30,12 @@
config
#(callback (types/json->clj %))))
+(defn enable-notifications []
+ (.enableNotifications (status)))
+
+(defn disable-notifications []
+ (.disableNotifications (status)))
+
(defn save-account-and-login
"NOTE: beware, the password has to be sha3 hashed"
[multiaccount-data hashed-password config accounts-data]
diff --git a/src/status_im/notifications/core.cljs b/src/status_im/notifications/core.cljs
new file mode 100644
index 0000000000..95b4a5dd03
--- /dev/null
+++ b/src/status_im/notifications/core.cljs
@@ -0,0 +1,13 @@
+(ns status-im.notifications.core
+ (:require [re-frame.core :as re-frame]
+ [status-im.native-module.core :as status]))
+
+(re-frame/reg-fx
+ ::enable
+ (fn [_]
+ (status/enable-notifications)))
+
+(re-frame/reg-fx
+ ::disable
+ (fn [_]
+ (status/disable-notifications)))
diff --git a/src/status_im/transport/message/core.cljs b/src/status_im/transport/message/core.cljs
index 81613925ca..7caf006cef 100644
--- a/src/status_im/transport/message/core.cljs
+++ b/src/status_im/transport/message/core.cljs
@@ -6,6 +6,7 @@
[status-im.utils.handlers :as handlers]
[status-im.ethereum.json-rpc :as json-rpc]
[status-im.ethereum.core :as ethereum]
+ [status-im.native-module.core :as status]
[status-im.transport.message.contact :as contact]
[status-im.transport.message.protocol :as protocol]
[status-im.transport.message.transit :as transit]
@@ -197,4 +198,3 @@
:params [confirmations]
:on-success #(log/debug "successfully confirmed messages")
:on-failure #(log/error "failed to confirm messages" %)}))))
-
diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs
index 9b93db55e5..8b03c28950 100644
--- a/src/status_im/ui/screens/profile/user/views.cljs
+++ b/src/status_im/ui/screens/profile/user/views.cljs
@@ -111,7 +111,7 @@
(defn- flat-list-content
[preferred-name registrar tribute-to-talk
active-contacts-count show-backup-seed?
- keycard-account?]
+ keycard-account? notifications-enabled?]
[(cond-> {:title (or (when registrar preferred-name)
:t/ens-usernames)
:subtitle (if registrar
@@ -151,15 +151,23 @@
[(when show-backup-seed?
[components.common/counter {:size 22} 1]) :chevron]
:on-press #(re-frame/dispatch [:navigate-to :privacy-and-security])}
- ;; TODO commented out for now because it will be enabled for android notifications
- #_{:icon :main-icons/notification
+ (when (and platform/android?
+ config/local-notifications?)
+ {:icon :main-icons/notification
:title :t/notifications
:accessibility-label :notifications-button
- ;; TODO commented out for now, uncomment when notifications-settings view
- ;; is populated. Then remove :on-press below
- ;; :on-press #(re-frame/dispatch [:navigate-to :notifications-settings])
- :on-press #(.openURL react/linking "app-settings://notification/status-im")
- :accessories [:chevron]}
+ :on-press
+ #(re-frame/dispatch
+ [:multiaccounts.ui/notifications-switched (not notifications-enabled?)])
+ :accessories
+ [[react/switch
+ {:track-color #js {:true colors/blue :false nil}
+ :value notifications-enabled?
+ :on-value-change
+ #(re-frame/dispatch
+ [:multiaccounts.ui/notifications-switched
+ (not notifications-enabled?)])
+ :disabled false}]]})
{:icon :main-icons/mobile
:title :t/sync-settings
:accessibility-label :sync-settings-button
@@ -212,7 +220,8 @@
seed-backed-up?
mnemonic
keycard-key-uid
- address]
+ address
+ notifications-enabled?]
:as multiaccount} @(re-frame/subscribe [:multiaccount])
active-contacts-count @(re-frame/subscribe [:contacts/active-count])
tribute-to-talk @(re-frame/subscribe [:tribute-to-talk/profile])
@@ -225,7 +234,7 @@
(flat-list-content
preferred-name registrar tribute-to-talk
active-contacts-count show-backup-seed?
- keycard-key-uid)
+ keycard-key-uid notifications-enabled?)
list-ref
scroll-y]))
diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs
index d0de80bc17..fc935432e9 100644
--- a/src/status_im/utils/config.cljs
+++ b/src/status_im/utils/config.cljs
@@ -31,6 +31,8 @@
(def max-message-delivery-attempts (js/parseInt (get-config :MAX_MESSAGE_DELIVERY_ATTEMPTS "6")))
(def contract-nodes-enabled? (enabled? (get-config :CONTRACT_NODES "0")))
(def mobile-ui-for-desktop? (enabled? (get-config :MOBILE_UI_FOR_DESKTOP "0")))
+;; NOTE: only disabled in releases
+(def local-notifications? (enabled? (get-config :LOCAL_NOTIFICATIONS "1")))
;; CONFIG VALUES
(def log-level