From 36ad6fb7629d71f811c690143ce6863151675e59 Mon Sep 17 00:00:00 2001 From: yenda Date: Mon, 21 Oct 2019 15:09:57 +0200 Subject: [PATCH] support for local notification service on Android - add option in profile on Android to enable local notifications - use foreground service to keep the app alive when running in the background - implement enable and disbable notification function in status module When enabling notifications, a foreground service is started that displays a sticky notification to make the user aware that the app is running in the background. Notifications are updated whenever a new.message signal is handled on java side. Currently only one to one chats are generating notifications but that can be easily extended to other types of messages, including mentions and keywords. The ens name of the user as well as keywords to follow should then be passed to the native side when calling the enable function. Signed-off-by: yenda --- .env.release | 1 + android/app/src/main/AndroidManifest.xml | 2 + externs.js | 2 + .../react-native-status/android/build.gradle | 2 +- .../ethereum/module/ForegroundService.java | 50 +++ .../module/NewMessageSignalHandler.java | 313 ++++++++++++++ .../status/ethereum/module/StatusModule.java | 392 ++++++++++-------- .../drawable-hdpi/ic_stat_notify_status.png | Bin 0 -> 640 bytes .../drawable-mdpi/ic_stat_notify_status.png | Bin 0 -> 429 bytes .../drawable-xhdpi/ic_stat_notify_status.png | Bin 0 -> 809 bytes .../drawable-xxhdpi/ic_stat_notify_status.png | Bin 0 -> 1143 bytes .../ic_stat_notify_status.png | Bin 0 -> 1480 bytes .../main/res/drawable/notification_icon.png | Bin 0 -> 648 bytes .../src/main/res/raw/notification_sound.mp3 | Bin 0 -> 11804 bytes .../maven-and-npm-deps/maven/maven-inputs.txt | 1 + .../maven/maven-sources.nix | 15 + src/status_im/multiaccounts/core.cljs | 56 ++- src/status_im/multiaccounts/login/core.cljs | 40 +- src/status_im/native_module/core.cljs | 6 + src/status_im/notifications/core.cljs | 13 + src/status_im/transport/message/core.cljs | 2 +- .../ui/screens/profile/user/views.cljs | 29 +- src/status_im/utils/config.cljs | 2 + 23 files changed, 701 insertions(+), 225 deletions(-) create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/module/ForegroundService.java create mode 100644 modules/react-native-status/android/src/main/java/im/status/ethereum/module/NewMessageSignalHandler.java create mode 100644 modules/react-native-status/android/src/main/res/drawable-hdpi/ic_stat_notify_status.png create mode 100644 modules/react-native-status/android/src/main/res/drawable-mdpi/ic_stat_notify_status.png create mode 100644 modules/react-native-status/android/src/main/res/drawable-xhdpi/ic_stat_notify_status.png create mode 100644 modules/react-native-status/android/src/main/res/drawable-xxhdpi/ic_stat_notify_status.png create mode 100644 modules/react-native-status/android/src/main/res/drawable-xxxhdpi/ic_stat_notify_status.png create mode 100644 modules/react-native-status/android/src/main/res/drawable/notification_icon.png create mode 100644 modules/react-native-status/android/src/main/res/raw/notification_sound.mp3 create mode 100644 src/status_im/notifications/core.cljs 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 0000000000000000000000000000000000000000..6466c90c39689954ed64d24ee9fad5dd6ade41c9 GIT binary patch literal 640 zcmV-`0)PF9P) zOQq7&)s;9Yw2Gzu6}YljWsz;H$m|4Jbpo8=7K?Fn1c`6;Ay^X}+k4Z#bUkA&s8uKG-wkN$+MV8!)ZL& z`>#b9^|H#Cx2S4Z?(qs=%={IU86sg_a1fK(;^X@*kde?*L4#)$`h|!YKHs!ZWFGnW zpyAgOOcdJtjn0`SJ~4x-*j;J98Nvw-DbLvJtY;2KoUkhjy+?dp5$}!2Y<92LJM6|N zaQGC;2@P^VwKcURk4%xdqlg#AGxEJ-U2Lajawqgttn*{z#ONsD$tuZDlL~?caZ|%B zxty)%4d*03ODYH>lihd$w&D;wS>;J@znq6rIyZZ%r!c}nnKxv=gn(=3#4w+Hx2KQyLJNEQn z(EKEycNgfmEueG-bjU6I>IpZ{hDsBb{*C;h)D5D4N0HL5^fx+mrRhJ|Fsz1SC?BvI aE&u=)2dJxIXZLUb0000VG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ef91b14a6443174056b5d37591e8f822f1448b8c GIT binary patch literal 429 zcmV;e0aE^nP)#SwaO2MPc}mpcBbRPvjZeBj;_nySro(4n0CY^n+7yeQ#LBUB~432 z`TxFa|C{MZp$j!-2)U)(>JksU$9MP*HU9>SE*YoWve~fZnJ%OkcAy5exF!-jyXY{% zu+;($3_frTu4Um+`cW{5JJb;}0^7o59=Q^-UD(HnfE~k8gK+JKW1Z9i%jXt$ptgJf zVA;=gp6A7YtH!Cp|8f`Fjm_L~^QqSArAP4)`1!9MncpJtG!tMBv!w!LlzXvM0AzlN zaN{t2uk(?a*&*|XNL_{o5biJp=`)3DF@;-cIVu+hT@UZ31J?mFVgU65L)(U97VcN! zkcLnv06!-5bbz210c2UvKT4ungEkEdfJ}DCMxIRdk-zP^##AuL+SDqLDRlO-n9@@= zWj30c@WCB72j1V1V0xo5cC+QY?x=R?c`pu{(y+gZ*U_S|qeF2`{|bllDw7_SKK%gz Xa+`tsNzf{v00000NkvXXu0mjfsO!S9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..014de5e163261b1eba621a991b7c9e777fd7f0ae GIT binary patch literal 809 zcmV+^1J?YBP)3a-<(&czB+dll7Bzn4sxOHZx9#7?;tApqCLLU*J8y!*BQx{)=le9XNAb#)MoO z;dD2`rKcx!7uP~Zh69HOm1mCvhz)@*=*$R@S7L&8!)Pi5x?v-A;lk+&45d*n&+f;8lDsfzr$n*f#b&6lJCWgF(#O4I6`SV%pWlEMe^@wpMjYU8$rm zCMDKi(mGVva&14$gdE(YzgvC4052~a@T7P%;Z|b~jc1(h=;-p*?3BS{mViHQB;1~f zY&9G13+ixIY$nKf&phQ8HtrVH`evWz#_3A8@CfrlY3E zTEJ$*X7gyu6faMp1qfQCxq!{FV(^ez_{eQRz&GwIyXrPLV2!Z=88r$YhbBT#Yv*Cn zwxeVTye6vuKladj0+#7$4BAXD4e!7Wgwa3Wy+cn>?@y@T_W!8&7nH4!Cbobz7Vm$7 zAG3o1tJM&KrLEbE|d$@AozU9)rvo(CNm)?WjeW2gwSc+UDUS*IL=opgj8JK8>6krBF&O zjagW-LDo^7YJj*-G6=4(bds$hu=Pzn<8-c(klpwcS>UIRPNQ!Y1huw3-S9oZ4r&-& z2;7GW`kac%bqEGqAs8)&Xt*2x@p|STaD?TEDSgNnlevOQSOk8|;F#4otkwlR=*G2Q n2Qp(&FEs#RoJ$Rc0T=}Up*lMJVPw9D00000NkvXXu0mjfyIz1{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..19432801ef4624e02b6f8abcb5344834d068a372 GIT binary patch literal 1143 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 literal 0 HcmV?d00001 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